PONY λ M2 Modula-2

JavaScript.CodeCompared.To/Ruby

An interactive executable cheatsheet comparing JavaScript and Ruby

JavaScript (ES2025) Ruby 4.0
Variables & Assignment
Variable assignment
let count = 0; const greeting = "Hello"; console.log(count); console.log(greeting);
count = 0 greeting = "Hello" puts count puts greeting
Ruby has no let, const, or var. Variables are created on first assignment. All local variables are mutable — Ruby has no equivalent of const for local variables (constants are a separate concept using UPPER_CASE).
Constants
const MAX_RETRIES = 3; const API_URL = "https://example.com"; console.log(MAX_RETRIES); console.log(API_URL);
MAX_RETRIES = 3 API_URL = "https://example.com" puts MAX_RETRIES puts API_URL
Ruby constants start with an uppercase letter (SCREAMING_SNAKE_CASE by convention). Unlike JavaScript's const, Ruby constants can be reassigned — Ruby issues a warning but does not raise an error. For true immutability, freeze the value: LIMIT = 100.freeze.
Multiple assignment
const [first, second, ...rest] = [1, 2, 3, 4, 5]; console.log(first); console.log(second); console.log(rest);
first, second, *rest = [1, 2, 3, 4, 5] puts first puts second puts rest.inspect
Ruby calls this "parallel assignment" or "multiple assignment." The * splat captures remaining elements — equivalent to JavaScript's rest operator .... The splat can appear anywhere in the pattern, not just at the end.
nil instead of null/undefined
let value = null; console.log(value === null); console.log(typeof undefined);
value = nil puts value.nil? puts value.inspect
Ruby has only nil — there is no undefined. Accessing an uninitialized local variable raises NameError immediately. nil is an object (NilClass) with its own methods, including nil?, to_a (returns []), and to_s (returns "").
Types & Truthiness
Falsy values
if (0) { console.log("JS: 0 is truthy"); } else { console.log("JS: 0 is falsy"); } if ("") { console.log("JS: '' is truthy"); } else { console.log("JS: '' is falsy"); }
if 0 then puts "Ruby: 0 is truthy" else puts "Ruby: 0 is falsy" end if "" then puts "Ruby: '' is truthy" else puts "Ruby: '' is falsy" end
In Ruby, only false and nil are falsy. Everything else — 0, "", [], {} — is truthy. This is a frequent source of bugs for developers coming from JavaScript, where 0, "", null, undefined, and NaN are all falsy.
Type checking
console.log(typeof 42); console.log(typeof "hello"); console.log(typeof true);
puts 42.is_a?(Integer) puts "hello".is_a?(String) puts true.is_a?(TrueClass)
Ruby uses is_a? (alias: kind_of?) to check types. There is no typeof operator. Every Ruby value is an object with a .class method — 42.class returns Integer, nil.class returns NilClass.
Inspecting an object's class
console.log([1, 2].constructor.name); console.log({}.constructor.name); console.log((42).constructor.name);
puts [1, 2].class puts({}.class) puts 42.class
In Ruby, .class returns the actual class object. Every value is an instance of a class: arrays are Array, plain hashes are Hash, and integers are Integer. Unlike JavaScript, there is no distinction between primitive types and objects — everything in Ruby is an object.
Type conversion
console.log(Number("42")); console.log(String(100)); console.log(Math.floor(3.14));
puts "42".to_i puts 100.to_s puts 3.14.to_i
Ruby uses method-based conversion: to_i (to integer), to_f (to float), to_s (to string), to_a (to array). There is no implicit coercion between types — "3" + 3 raises TypeError rather than producing "33" or 6.
Strings
String interpolation
const name = "World"; console.log(`Hello, ${name}!`);
name = "World" puts "Hello, #{name}!"
Ruby uses "..." with #{} for interpolation. Single-quoted strings ('...') do not interpolate — 'Hello, #{name}' is literal text. In Ruby 4.0, string literals are frozen by default; use +"mutable" or String.new("mutable") to get a mutable string.
Common string methods
const text = "Hello, World!"; console.log(text.toUpperCase()); console.log(text.includes("World")); console.log(text.length);
text = "Hello, World!" puts text.upcase puts text.include?("World") puts text.length
Ruby string methods use snake_case rather than camelCase: upcase/downcase instead of toUpperCase/toLowerCase, include? instead of includes. Methods that return a boolean conventionally end with ?.
String slicing
const text = "Hello, World!"; console.log(text.slice(7, 12)); console.log(text.slice(-1));
text = "Hello, World!" puts text[7, 5] puts text[-1]
Ruby's string[start, length] takes a start index and a length (not an end index), unlike JavaScript's slice(start, end). string[range] with a Range object also works. Negative indices count from the end in both languages.
Multiline strings & heredocs
const message = `Line one Line two Line three`; console.log(message);
message = <<~MSG Line one Line two Line three MSG puts message
Ruby's <<~HEREDOC (squiggly heredoc) strips leading whitespace, so the content can be indented with the surrounding code. The closing delimiter can be any word. Ruby also supports regular multiline strings using "...", but heredocs are idiomatic for longer text.
split and join
const words = "one two three".split(" "); console.log(words.join("-"));
words = "one two three".split(" ") puts words.join("-")
Ruby's split and join work identically to JavaScript's — these are among the smoothest transitions between the two languages. split with no arguments splits on whitespace and removes empty strings, like Python's default split behavior.
Symbols
Symbols
const status = Symbol("active"); console.log(status.toString()); console.log(typeof status);
status = :active puts status puts status.class
Ruby symbols (:name) are immutable, interned objects — :active always refers to the exact same object in memory, making them fast to compare. They are lighter than strings and widely used as hash keys, method names, and identifiers. Unlike JavaScript's Symbol(), Ruby symbols with the same name are identical: :foo.equal?(:foo) is always true.
Symbols as hash keys
const person = { name: "Alice", age: 30 }; console.log(person.name); console.log(person["age"]);
person = { name: "Alice", age: 30 } puts person[:name] puts person[:age]
Ruby's { key: value } hash syntax creates symbol keys (e.g., :name), but accessing them requires bracket notation: hash[:name] — not hash.name. This surprises JavaScript developers who expect dot-access on plain hashes.
Symbol#to_proc shorthand
const words = ["hello", "world"]; console.log(words.map(word => word.toUpperCase()).join(", "));
words = ["hello", "world"] puts words.map(&:upcase).join(", ")
&:method_name converts a symbol to a proc that calls that method on each element — shorthand for { |item| item.method_name }. It is Ruby's most concise callback pattern and works with any method that takes no arguments: map(&:to_s), select(&:even?), reject(&:nil?).
Arrays
Array basics
const numbers = [1, 2, 3]; console.log(numbers.length); console.log(numbers[0]); console.log(numbers[numbers.length - 1]);
numbers = [1, 2, 3] puts numbers.length puts numbers.first puts numbers.last
Ruby arrays share the same literal syntax as JavaScript. In addition to index access (numbers[0]), Ruby provides first and last as named methods — these read more clearly when the intent is to access the beginning or end of a collection.
push, pop, and append
const items = [1, 2]; items.push(3); console.log(items); console.log(items.pop()); console.log(items);
items = [1, 2] items.push(3) puts items.inspect puts items.pop puts items.inspect
Ruby's push and pop work identically to JavaScript's. Ruby also uses << as an alias for push — you will see items << 4 frequently in Ruby code. shift and unshift operate on the front of the array, just as in JavaScript.
flatten and compact
const nested = [1, [2, [3]]]; console.log(nested.flat(Infinity)); console.log([1, null, 2, undefined, 3].filter(v => v != null));
nested = [1, [2, [3]]] puts nested.flatten.inspect puts [1, nil, 2, nil, 3].compact.inspect
flatten removes all nesting (pass a depth integer to limit). compact removes nil values — the Ruby equivalent of filter(v => v != null). Note that compact only removes nil, not false or 0, since those are truthy in Ruby.
Slicing arrays
const numbers = [1, 2, 3, 4, 5]; console.log(numbers.slice(1, 3)); console.log(numbers.slice(-2));
numbers = [1, 2, 3, 4, 5] puts numbers[1, 2].inspect puts numbers[-2..].inspect
Ruby's array[start, length] takes a length argument (not an end index). array[range] with a Range object also works — the endless range n.. captures from index n to the end. JavaScript's slice(1, 3) (indices 1 and 2) maps to Ruby's [1, 2] (start 1, length 2).
Hashes
Hash literals
const config = { host: "localhost", port: 3000 }; console.log(config.host); console.log(config["port"]);
config = { host: "localhost", port: 3000 } puts config[:host] puts config[:port]
Ruby hashes look like JavaScript objects but require bracket notation for access: hash[:key]. There is no dot-access on plain hashes. The { key: value } shorthand creates symbol keys — use { "key" => value } if you need string keys instead.
Safe key access with fetch
const data = { name: "Alice" }; console.log(data.age ?? "unknown"); console.log(data.name);
data = { name: "Alice" } puts data.fetch(:age, "unknown") puts data[:name]
fetch raises KeyError when the key is missing (unlike [], which returns nil). Passing a second argument provides a default — similar to JavaScript's nullish coalescing ??. You can also pass a block: fetch(:key) { |k| compute_default(k) }.
Merging hashes
const defaults = { color: "blue", size: 10 }; const custom = { size: 20 }; console.log({ ...defaults, ...custom });
defaults = { color: "blue", size: 10 } custom = { size: 20 } result = defaults.merge(custom) puts result.inspect
merge returns a new hash — when keys overlap, the argument hash wins, just like JavaScript's spread operator. merge! (or update) modifies the receiver in place. Ruby hashes maintain insertion order (since Ruby 1.9), matching JavaScript object behavior.
Iterating over hashes
const scores = { alice: 95, bob: 87 }; Object.entries(scores).forEach(([name, score]) => { console.log(`${name}: ${score}`); });
scores = { alice: 95, bob: 87 } scores.each do |name, score| puts "#{name}: #{score}" end
Ruby hashes have a built-in each that yields key-value pairs directly — no need for Object.entries. The block receives two parameters: the key and the value. map, select, reject, and other Enumerable methods also work on hashes.
Ranges
Range literals
const numbers = Array.from({ length: 10 }, (_, index) => index + 1); console.log(numbers.join(", ")); console.log(numbers.includes(5));
puts (1..10).to_a.join(", ") puts (1..10).include?(5)
Ruby has a built-in Range type: 1..10 is inclusive (includes 10), and 1...10 is exclusive (excludes 10). Ranges work on any comparable type: 'a'..'z', Date.today..Date.today + 7. They are lazy — converting to an array with to_a is only needed when you want an actual array.
Range step
const evens = Array.from({ length: 5 }, (_, index) => (index + 1) * 2); console.log(evens.join(", "));
puts (2..10).step(2).to_a.join(", ")
Range#step iterates with a custom increment. It returns an Enumerator (lazy), so chaining to_a, map, or each on it is normal. For floating-point steps, use (0.0..1.0).step(0.1).
Ranges in case/when
function grade(score) { if (score >= 90) return "A"; if (score >= 80) return "B"; if (score >= 70) return "C"; return "F"; } console.log(grade(85));
def grade(score) case score when 90..100 then "A" when 80...90 then "B" when 70...80 then "C" else "F" end end puts grade(85)
Ruby's case/when uses === for matching, and Range#=== checks membership. This makes range-based conditionals extremely readable. The exclusive range 80...90 ensures score 90 matches the "A" arm above, not "B".
Control Flow
if and unless
const active = false; if (!active) { console.log("inactive"); }
active = false unless active puts "inactive" end
unless is the logical inverse of if — equivalent to if !condition. It reads more naturally when the condition is already negative. Similarly, until is the inverse of while. Avoid double-negatives: prefer unless active over if !active.
if as an expression
const x = 10; const label = x > 5 ? "big" : "small"; console.log(label);
x = 10 label = if x > 5 then "big" else "small" end puts label
In Ruby, if, unless, case, and begin are all expressions — they return the last evaluated value. Assigning an if to a variable is idiomatic Ruby, though the ternary operator ? : is also available for the same pattern.
case/when vs switch
const day = "Mon"; let kind; switch (day) { case "Mon": case "Tue": case "Wed": case "Thu": case "Fri": kind = "weekday"; break; default: kind = "other"; } console.log(kind);
day = "Mon" result = case day when "Mon", "Tue", "Wed", "Thu", "Fri" then "weekday" when "Sat", "Sun" then "weekend" else "unknown" end puts result
Ruby's case/when does not fall through between cases — no break is needed. when accepts multiple comma-separated values, ranges, and regular expressions. The whole expression returns a value, so assigning it directly is idiomatic.
Guard clauses & trailing if
function process(data) { if (data === null || data === undefined) return "no data"; return data.toUpperCase(); } console.log(process(null)); console.log(process("hello"));
def process(data) return "no data" if data.nil? data.upcase end puts process(nil) puts process("hello")
Ruby allows if and unless as trailing modifiers: return "no data" if data.nil?. This reads as plain English and is idiomatic for guard clauses. The last expression in a method is its return value — no return keyword needed for the happy path.
Methods
Defining methods
function greet(name) { return `Hello, ${name}!`; } console.log(greet("Alice"));
def greet(name) "Hello, #{name}!" end puts greet("Alice")
Ruby methods return the last evaluated expression — no return keyword is needed for the normal return path (use it only for early returns). Method names use snake_case by convention. Parentheses on method calls are optional when unambiguous.
Default parameters
function greet(name = "stranger") { return `Hello, ${name}!`; } console.log(greet()); console.log(greet("Alice"));
def greet(name = "stranger") "Hello, #{name}!" end puts greet puts greet("Alice")
Ruby default parameter syntax is identical to JavaScript's. The default can reference earlier parameters or any expression, including method calls. Unlike JavaScript, Ruby evaluates the default expression fresh each time the method is called.
Keyword arguments
function createUser({ name, role = "user" } = {}) { return `${name} (${role})`; } console.log(createUser({ name: "Alice", role: "admin" }));
def create_user(name:, role: "user") "#{name} (#{role})" end puts create_user(name: "Alice", role: "admin")
Ruby has first-class keyword arguments declared in the method signature with :. Required keywords (no default) raise ArgumentError if omitted. Callers must use the keyword name — create_user(name: "Alice") — making calls self-documenting without relying on parameter order.
Variadic arguments (splat)
function sum(...numbers) { return numbers.reduce((total, n) => total + n, 0); } console.log(sum(1, 2, 3, 4));
def sum(*numbers) numbers.sum end puts sum(1, 2, 3, 4)
Ruby's *args (splat) is the equivalent of JavaScript's ...args (rest). Inside the method, numbers is an ordinary array. Ruby also has **kwargs for capturing keyword arguments into a hash, and &block for capturing a passed block as a Proc.
Predicate and bang methods
function isEmpty(arr) { return arr.length === 0; } function uppercased(str) { return str.toUpperCase(); } console.log(isEmpty([])); console.log(uppercased("hello"));
def empty?(collection) collection.empty? end words = ["hello"] puts empty?([]) puts words.map(&:upcase).join
Ruby method names can end with ? (predicates that return boolean, e.g. empty?, nil?) or ! (methods that modify in place or raise instead of returning nil, e.g. sort!, map!). This is a naming convention — both characters are valid in method names.
Blocks
Blocks
[1, 2, 3].forEach(number => { console.log(number * 2); });
[1, 2, 3].each do |number| puts number * 2 end
A Ruby block is an anonymous chunk of code passed to a method, delimited by { |params| ... } (single-line convention) or do |params| ... end (multi-line convention). Unlike JavaScript callbacks, blocks are not objects by default — they are passed implicitly and received by the method via yield.
yield
function withLogging(callback) { console.log("start"); callback(); console.log("end"); } withLogging(() => console.log("work"));
def with_logging puts "start" yield puts "end" end with_logging { puts "work" }
yield calls the block that was passed to the method. If no block is given, yield raises LocalJumpError. Use block_given? to check whether a block was passed before yielding. yield can pass arguments to the block: yield value passes value to the block parameter.
Capturing a block as a Proc
function apply(value, transform) { return transform(value); } console.log(apply(5, n => n * 2));
def apply(value, &transform) transform.call(value) end puts apply(5) { |n| n * 2 }
Adding &block_name as the last parameter captures the block as a Proc object, which can be stored, passed to other methods, or called with .call(). This makes the block explicit and composable. &: also converts a Proc back to a block when passing it to another method.
Iterators & Enumerable
map
const numbers = [1, 2, 3]; console.log(numbers.map(n => n * n).join(", "));
numbers = [1, 2, 3] puts numbers.map { |n| n * n }.join(", ")
Ruby's map (also aliased as collect) works identically to JavaScript's. Both return a new array. map! modifies the array in place. Ruby's Enumerable module provides map and dozens of other iteration methods to any class that implements each.
select and reject vs filter
const numbers = [1, 2, 3, 4, 5]; console.log(numbers.filter(n => n % 2 === 0).join(", ")); console.log(numbers.filter(n => n % 2 !== 0).join(", "));
numbers = [1, 2, 3, 4, 5] puts numbers.select { |n| n.even? }.join(", ") puts numbers.reject { |n| n.even? }.join(", ")
Ruby's select (also filter) keeps elements where the block is truthy. reject is its complement — it removes elements where the block is truthy, equivalent to filter(x => !condition). Having both makes intent clearer: choose the one that reads more naturally for your condition.
reduce / inject
const numbers = [1, 2, 3, 4]; console.log(numbers.reduce((total, n) => total + n, 0));
numbers = [1, 2, 3, 4] puts numbers.reduce(0) { |total, n| total + n } puts numbers.reduce(:+)
Ruby's reduce (also inject) puts the accumulator first in the block parameters: |total, element|. With a symbol argument, reduce(:+) is shorthand for reduce { |sum, n| sum + n }. If no initial value is given, the first element is used as the seed.
each_with_index
["a", "b", "c"].forEach((item, index) => { console.log(`${index}: ${item}`); });
["a", "b", "c"].each_with_index do |item, index| puts "#{index}: #{item}" end
Both each_with_index and JavaScript's forEach yield the element first and the index second. Ruby also provides each.with_index(start) to offset the index counter, and each_with_object for accumulating into a mutable object while iterating.
each_with_object
const words = ["cat", "dog", "cow"]; const groups = words.reduce((acc, word) => { const key = word[0]; acc[key] = (acc[key] || []); acc[key].push(word); return acc; }, {}); console.log(JSON.stringify(groups));
words = ["cat", "dog", "cow"] groups = words.each_with_object(Hash.new { |h, k| h[k] = [] }) do |word, hash| hash[word[0]] << word end puts groups.inspect
each_with_object passes an accumulator as the second block parameter and always returns it — you do not need to return it explicitly from the block. It is cleaner than reduce when building a mutable object. Hash.new { |h, k| h[k] = [] } creates a hash with a default value computed by a block.
sort and sort_by
const words = ["banana", "kiwi", "strawberry", "fig"]; console.log(words.slice().sort().join(", ")); console.log(words.slice().sort((a, b) => a.length - b.length).join(", "));
words = ["banana", "kiwi", "strawberry", "fig"] puts words.sort.join(", ") puts words.sort_by { |word| word.length }.join(", ")
sort_by extracts a sort key from each element — cleaner and faster than sort { |a, b| a.length <=> b.length }. The <=> spaceship operator returns -1, 0, or 1 and is the basis of all Ruby sorting. Ruby's sort does not mutate in place — use sort! for that.
flat_map
const sentences = ["hello world", "foo bar"]; console.log(sentences.flatMap(sentence => sentence.split(" ")).join(", "));
sentences = ["hello world", "foo bar"] puts sentences.flat_map { |sentence| sentence.split(" ") }.join(", ")
flat_map (also collect_concat) maps then flattens one level. It is equivalent to .map { ... }.flatten(1) but more efficient. Use it whenever each element maps to an array and you want a single flat result.
Procs & Lambdas
Proc
const double = x => x * 2; console.log(double(5));
double = proc { |x| x * 2 } puts double.call(5)
A Proc is a first-class function object. proc { |x| ... } creates a block-wrapper that can be stored and passed. Procs have "loose" argument handling — extra arguments are ignored and missing ones become nil. return inside a proc returns from the enclosing method, not just the proc.
Lambda
const multiply = (x, y) => x * y; console.log(multiply(3, 4));
multiply = ->(x, y) { x * y } puts multiply.call(3, 4)
-> (stabby lambda) is the modern lambda syntax. Lambdas enforce argument count — passing the wrong number raises ArgumentError, unlike proc. return inside a lambda returns from the lambda only, not the enclosing method. Lambdas are the closer Ruby equivalent to JavaScript arrow functions.
Method references
function double(x) { return x * 2; } console.log([1, 2, 3].map(double).join(", "));
def double(x) = x * 2 puts [1, 2, 3].map(&method(:double)).join(", ")
method(:name) returns a Method object for a named method. Combined with &, it converts the method to a block — allowing named methods to be passed where blocks are expected. The one-liner method syntax def double(x) = x * 2 (Ruby 3.0+) is a concise way to define simple methods.
Classes & OOP
Class definition
class Animal { constructor(name) { this.name = name; } speak() { return `${this.name} makes a sound`; } } const animal = new Animal("Cat"); console.log(animal.speak());
class Animal def initialize(name) @name = name end def speak "#{@name} makes a sound" end end puts Animal.new("Cat").speak
Ruby uses initialize instead of constructor, and new is a class method rather than a keyword. Instance variables are prefixed with @. There is no this — you access the current object's state through @instance_vars and call its methods by name.
attr_reader, attr_writer, attr_accessor
class Person { constructor(name) { this._name = name; } get name() { return this._name; } set name(value) { this._name = value; } } const person = new Person("Alice"); console.log(person.name); person.name = "Bob"; console.log(person.name);
class Person attr_accessor :name def initialize(name) @name = name end end person = Person.new("Alice") puts person.name person.name = "Bob" puts person.name
attr_accessor :name generates both a getter (name) and a setter (name=). attr_reader generates only a getter; attr_writer only a setter. These replace explicit get name() / set name(v) definitions and are idiomatic Ruby.
Inheritance
class Animal { constructor(name) { this.name = name; } speak() { return `${this.name} speaks`; } } class Dog extends Animal { speak() { return `${this.name} barks`; } } console.log(new Dog("Rex").speak());
class Animal def initialize(name) @name = name end def speak "#{@name} speaks" end end class Dog < Animal def speak "#{@name} barks" end end puts Dog.new("Rex").speak
Ruby uses < for single inheritance. super calls the parent method — bare super forwards all arguments; super(args) passes specific ones; super() passes no arguments. Ruby does not support multiple inheritance of classes — use modules (mixins) instead.
Class (static) methods
class Counter { static count = 0; static increment() { return ++Counter.count; } } console.log(Counter.increment()); console.log(Counter.increment());
class Counter @@count = 0 def self.increment @@count += 1 end end puts Counter.increment puts Counter.increment
Ruby uses def self.method_name to define class methods (equivalent to JavaScript's static). Class variables (@@name) are shared across the class and all subclasses — many Rubyists prefer class-level instance variables (@count defined in a class method) to avoid this surprising inheritance behavior.
Modules & Mixins
Modules as namespaces
const Auth = { login(username) { return `${username} logged in`; } }; console.log(Auth.login("Alice"));
module Auth def self.login(username) "#{username} logged in" end end puts Auth.login("Alice")
Ruby modules serve as namespaces (grouping related methods and constants under a common name) and as mixins (sharing behavior across classes). They cannot be instantiated — Module.new is possible but Auth.new raises NoMethodError.
include (mixin)
function withGreeting(name) { return { name, greet() { return `Hello, I'm ${this.name}`; } }; } const person = withGreeting("Alice"); console.log(person.greet());
module Greetable def greet "Hello, I'm #{name}" end end class Person include Greetable attr_reader :name def initialize(name) @name = name end end puts Person.new("Alice").greet
include brings a module's instance methods into a class. The module's methods look up self at runtime, so they can call methods defined on the including class — here, greet calls name, which is provided by attr_reader. A class can include multiple modules, making this Ruby's answer to multiple inheritance.
Comparable mixin
class Weight { constructor(value) { this.value = value; } } const weights = [new Weight(5), new Weight(2), new Weight(8)]; weights.sort((a, b) => a.value - b.value); console.log(weights.map(w => w.value).join(", "));
class Weight include Comparable attr_reader :value def initialize(value) @value = value end def <=>(other) value <=> other.value end end weights = [Weight.new(5), Weight.new(2), Weight.new(8)] puts weights.sort.map(&:value).join(", ")
Including Comparable and defining <=> (spaceship operator) gives the class <, >, <=, >=, between?, clamp, and sort support automatically. Enumerable similarly grants map, select, reduce, etc. to any class that defines each.
Error Handling
begin/rescue vs try/catch
try { JSON.parse("not json"); } catch (error) { console.log("Caught:", error.message); }
begin Integer("not a number") rescue ArgumentError => error puts "Caught: #{error.message}" end
Ruby's rescue catches exceptions. You specify the exception class after rescue (like Java's typed catch) — rescuing without a class catches StandardError and its subclasses. Multiple rescue clauses can handle different exception types, matched top-to-bottom.
ensure vs finally
try { console.log("try"); throw new Error("oops"); } catch (e) { console.log("catch:", e.message); } finally { console.log("finally"); }
begin puts "begin" raise "oops" rescue RuntimeError => error puts "rescue: #{error.message}" ensure puts "ensure" end
ensure runs regardless of whether an exception was raised or rescued — equivalent to JavaScript's finally. It is commonly used for cleanup: closing files, releasing locks, or resetting state. The return value of ensure is discarded; it does not affect the value of the whole begin expression.
Raising exceptions
function divide(dividend, divisor) { if (divisor === 0) throw new Error("division by zero"); return dividend / divisor; } try { console.log(divide(10, 0)); } catch (error) { console.log(error.message); }
def divide(dividend, divisor) raise ArgumentError, "division by zero" if divisor == 0 dividend / divisor end begin puts divide(10, 0) rescue ArgumentError => error puts error.message end
Ruby uses raise (or its alias fail) to throw exceptions — raise ExceptionClass, "message". Using a specific exception class lets callers rescue only the errors they expect. raise with no arguments inside a rescue block re-raises the current exception.
Custom exception classes
class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; } } try { throw new ValidationError("invalid input"); } catch (error) { console.log(`${error.name}: ${error.message}`); }
class ValidationError < StandardError; end begin raise ValidationError, "invalid input" rescue ValidationError => error puts "#{error.class}: #{error.message}" end
Custom exception classes inherit from StandardError (not Exception, which also catches signals and fatal errors). A one-liner class body is idiomatic when no custom behavior is needed — StandardError provides message and backtrace automatically.
Pattern Matching
Hash pattern matching
const response = { status: "success", result: 42 }; if (response.status === "success") { console.log("Result:", response.result); }
response = { status: "success", result: 42 } case response in { status: "success", result: Integer => result } puts "Result: #{result}" in { status: "error", message: String => message } puts "Error: #{message}" end
Ruby's case/in (introduced in Ruby 3.0) matches and deconstructs in one step. Integer => result both checks the type and binds the value. Hash patterns match by key presence — extra keys in the hash are allowed by default. Unmatched patterns raise NoMatchingPatternError.
Array pattern matching
const [x, y, z] = [10, 20, 30]; console.log(x, y, z);
case [10, 20, 30] in [x, y, z] puts "#{x}, #{y}, #{z}" end
Array patterns in case/in deconstruct positionally. Unlike regular destructuring assignment, array patterns can include type checks: [Integer => x, Integer => y] matches and validates. Patterns can also be nested for deep deconstruction.
Find pattern
const data = [1, 2, "error", 3]; const message = data.find(item => typeof item === "string"); console.log(message);
data = [1, 2, "error", 3] case data in [*, String => message, *] puts message end
The find pattern [*, target_pattern, *] searches for a matching element anywhere in an array — like Array#find but integrated into pattern matching with deconstruction and type checking. The * wildcards capture any number of elements before and after the match.