Ruby Cheatsheet - Ruby Syntax & Methods Reference
This reference is for developers picking up Ruby (including Rails) or recalling its idioms. It starts with variables, symbols, and common built-in methods, then arrays/hashes, control flow (including postfix if and unless), methods and blocks/Proc/Lambda, classes & modules with attr_accessor, include/extend, and inheritance, and ends with begin-rescue-ensure exception handling. Unlike many languages, almost everything in Ruby is an object and methods return the value of their last expression, and blocks elegantly wrap iteration. After reading you should be able to write idiomatic, concise Ruby: iterate with blocks instead of hand loops, use symbols as hash keys, and organize reusable behavior into modules.
Typical Use Case
For developers writing Ruby scripts, reshaping data or maintaining Rails code. The confusion concentrates in a few places: the real difference between a symbol and a string, the three ways blocks travel and are invoked (yield, Proc.new, lambda), the reader and writer methods attr_accessor generates, and the collection chain from map through select, reject, partition and group_by. Most real tasks are one expression — filter, transform, group, reduce — and this sheet gives you the vocabulary for it. Classes and mixins, exception handling, and catch with throw cover the longer-term organisation needs.
Basics 7
name = "Ruby"name = "Hello, #{name}":symbol# single-line comment=begin
comment block
=endniltrue / falseArray & Hash 9
arr = [1, 2, 3]arr << 4arr[0]arr.first / arr.lasthash = { a: 1, b: 2 }hash[:a]hash[:c] = 3hash.key?(:a)hash.keys / hash.valuesControl Flow 10
if x > 0
puts "positive"
endputs "positive" if x > 0unless x > 0
puts "not positive"
endcase x
when 1 then "one"
else "other"
end3.times { puts "hi" }1.upto(5) { |n| puts n }5.downto(1) { |n| puts n }loop { break if done }while x > 0
x -= 1
enduntil x == 0
x -= 1
endMethods 9
def greet(name)
"Hello, #{name}"
enddef greet(name = "World")def sum(*args)def method(a:, b:)return valuedef method
yield if block_given?
endmy_proc = Proc.new { |x| x*2 }my_lambda = ->(x) { x*2 }proc.call(5)Class & Module 10
class MyClass
endattr_accessor :nameattr_reader :nameattr_writer :nameclass Child < Parentmodule MyModule
endinclude MyModuleextend MyModuleself.class_methodclass << self
endException 7
begin
risky_call
rescue
"error"
endrescue SpecificError => eensure
cleanup
endraise "error message"raise ArgumentError, "msg"retrycatch(:done) { throw(:done) }Built-in Methods 10
arr.map { |x| x*2 }arr.select { |x| x.even? }arr.reject { |x| x.even? }arr.reduce(0) { |s, x| s + x }hash.merge({ c: 3 })"hello".upcase"hello".split("")"hello".gsub(/l/, "L")arr.flattenarr.uniqBlocks & Iterators 7
[1, 2].each { |n| puts n }[1, 2].each do |n|
puts n
end(1..5).map { |n| n**2 }[1, 2, 3].select(&:even?)arr.partition { |x| x.even? }arr.chunk { |x| x > 0 }arr.group_by { |x| x % 3 }Parameter matrix
| Parameter | Effect | Example |
|---|---|---|
name = "Ruby" | Assigns to a dynamically typed local; no type declaration is involved. | name = "Ruby" |
"Hello, #{name}" | Interpolates a variable inside a double-quoted string. | puts "Hello, #{name}" |
:symbol | A symbol is immutable and unique, which makes it a good hash key. | hash = { a: 1, b: 2 } |
arr << 4 | Appends an element to the end of an array. | arr = [1, 2, 3]; arr << 4 |
arr.first / arr.last | Takes the first element / the last element. | arr.first |
hash.key?(:a) | Tests whether a key is present. | hash.key?(:a) |
hash.keys / hash.values | Returns all keys / all values. | hash.keys |
puts "positive" if x > 0 | Postfix if, the modifier form of a conditional. | puts "positive" if x > 0 |
unless x > 0 ... end | Runs the body when the condition does not hold, equivalent to a negated if. | unless x > 0; puts "not positive"; end |
case x when 1 then ... else ... end | Matches one value against several branches. | case x; when 1 then "one"; else "other"; end |
3.times { ... } | Repeats the block a fixed number of times. | 3.times { puts "hi" } |
def greet(name = "World") | Defines a method with a default argument value. | def greet(name = "World"); "Hello, #{name}"; end |
def sum(*args) | A splat parameter accepts a variable number of arguments. | def sum(*args); args.sum; end |
def method(a:, b:) | Keyword arguments, which may be supplied in any order. | def config(a:, b:); end |
def method; yield if block_given?; end | Calls the block that the caller passed in. | def each_x; yield if block_given?; end |
Proc.new { |x| x*2 } / ->(x) { x*2 } | Creates a Proc object, or a lambda using the arrow syntax. | my_lambda = ->(x) { x * 2 }; my_lambda.call(5) |
attr_accessor :name | Generates a reader and a writer in one declaration. | attr_accessor :name |
include MyModule / extend MyModule | Mixes instance methods in / mixes class methods in. | include MyModule |
arr.map { |x| x*2 } | Transforms the array and returns a new one. | arr.map { |x| x * 2 } |
arr.select / arr.reject | Keeps the elements that pass the test / drops the elements that do. | arr.select { |x| x.even? } |
arr.reduce(0) { |s, x| s + x } | Folds the array into a single accumulated value. | arr.reduce(0) { |s, x| s + x } |
arr.group_by { |x| x % 3 } | Groups elements by a computed key and returns a hash. | arr.group_by { |x| x % 3 } |
arr.partition { |x| x.even? } | Splits the array into the matching and the non-matching group in one pass. | arr.partition { |x| x.even? } |
rescue SpecificError => e | Catches a specific exception class and binds it to a variable. | begin; risky_call; rescue SpecificError => e; end |
ensure ... end | Runs cleanup code whether the body succeeded or raised. | begin; risky_call; ensure; cleanup; end |
raise ArgumentError, "msg" | Raises an exception of a given class. | raise ArgumentError, "msg" |
catch(:done) { throw(:done) } | A non-local jump, used to bail out of deeply nested loops. | catch(:done) { throw(:done) } |
Common pitfalls
SymptomLooking a hash up with a string key returns nil even though the value was just written.
CauseA symbol and a string are different objects; hash lookups match on type and equality, and keys are never coerced.
FixSettle on one convention, normally symbol keys such as hash[:a], or convert explicitly at the read site with hash[sym.to_s] or hash[key.to_sym].
Symptommap was used to change an array in place, yet the original array is untouched and later code reads stale data.
Causemap, select and reject all return new arrays and leave the receiver alone; only forms such as << or []= mutate it.
FixCapture the return value, for example arr = arr.map { |x| x * 2 }, or switch to the bang variants such as select! and map!.
SymptomAfter a break inside a loop, a large block of outer code is skipped and the execution order looks random.
CauseA break inside a block terminates the enclosing loop of the whole method call rather than only the block it appears in.
FixUse next to skip just one iteration, and reach for a catch(:done) with throw(:done) pair when the jump has to escape several nesting levels.
SymptomA method with no explicit return hands back an intermediate value left over from inside a block.
CauseA Ruby method returns the value of its last expression, and assignments or traversals inside a block change what that last expression turns out to be.
FixPut the intended value on the final line of the method, or write return explicitly.
SymptomA method defined with yield fails when called plainly, with a LocalJumpError.
Causeyield only has a block to run when the caller supplies one; without a block there is simply nothing to execute.
FixGuard with block_given? before yielding, or add a block at the call site using { ... } or do ... end.
SymptomA begin and rescue pair fails to catch anything and the exception still reaches the top level.
CauseA rescue clause only protects the begin it is attached to; placing it at a different nesting level creates a separate scope and the exception escapes.
FixWrap the risky call entirely in a begin ... rescue => e ... ensure ... end structure and check that the indentation levels line up.
SymptomAdding a batch of methods to a class silently overwrote a same-named instance method.
CauseMethods from an included module enter the ancestor chain above the class itself, so they win over same-named definitions in the class.
FixDraw the boundary explicitly: general capabilities belong in modules included with include, logic that serves only this class stays defined in the class, and class-level methods are mixed in separately with extend.
SymptomA long single-line block glues itself to the neighbouring call and breakpoints land in the wrong place.
CauseCurly braces bind tightly, which makes them easy to confuse with surrounding code inside a long expression, and do...end follows different binding rules in several situations.
FixUse do ... end for multi-line blocks and reserve { } for single-line ones, and let editor quick fixes such as Alt+Enter help locate the boundary.
Troubleshooting
1Confirm the key type and contents of a hash
hash.keys; hash.key?(:a); hash[:a]; hash["a"]The keys output shows whether keys are symbols or strings. Probe the same data once with :a and once with "a": only the matching type returns a value and the other returns nil, which pins the mismatch down immediately.
2Establish what an array actually holds
arr = [1, 2, 3]; arr.map { |x| x * 2 }; arr; arr.select { |x| x.even? }; arr.uniqmap returns a new array and leaves arr alone, select returns a filtered new array, and uniq removes duplicates. To confirm whether a change took effect, inspect arr itself rather than an intermediate result in a chain.
3Tell a single split apart from repeated filtering
arr = [1,2,3,4,5]; even, odd = arr.partition { |x| x.even? }; even; oddpartition walks the array once and returns two arrays that multiple assignment unpacks directly. If the result looks wrong, the predicate no longer matches the element type, for instance because the elements are strings.
4Group and count by a condition
arr.group_by { |x| x % 3 }; arr.group_by { |x| x % 3 }.map { |k, v| [k, v.size] }group_by returns a hash from key to the array of matching elements, and taking the size of each value yields the per-group counts, which is a quick check that the grouping logic behaves.
5Verify the block really ran
def each_x; yield if block_given?; "done"; end; each_x { |v| puts v }No output at all means the block_given? check failed or no block was passed; a returned "done" means the method body ran to the end, which distinguishes "the block did not run" from "the method was never called".
6Fold a sum while staying safe on an empty array
arr.reduce(0) { |s, x| s + x }; [].reduce(0) { |s, x| s + x }Passing an initial 0 lets an empty array return 0; dropping it and calling reduce with only a block raises on an empty array, so always pass the seed explicitly in scripts.
Command Examples
Chain enumerables so the data cleanup reads top to bottom
arr = [1, 2, 3, 4, 5, 6]
arr.select(&:even?).map { |x| x * 10 }.reduce(0) { |s, x| s + x }select, map and reduce each return new collections and leave the original array untouched, so chaining them is safe. &:even? is shorthand that converts a method name into the symbol the block calls, removing a |x| boilerplate.
Output
=> 120
Group data by condition to see the distribution
arr = [1, 2, 3, 4, 5]
arr.group_by { |x| x % 3 }
arr.partition { |x| x > 3 }group_by returns a hash, which suits per-bucket statistics; partition returns two arrays where the first element is the passing group — the order is fixed but easy to remember backwards.
Output
{0=>[3], 1=>[1, 4], 2=>[2, 5]}
[[4, 5], [1, 2, 3]]String substitution and splitting
log = "2024-05-01 ERROR timeout"
log.gsub(/\d{4}-\d{2}-\d{2}/, "DATE")
log.split(" ")gsub needs a regex to match patterns like a date; a plain string only does literal replacement. gsub returns a new string and never mutates the original, so there is no destructive_ variant to reach for.
Output
=> "DATE ERROR timeout" => ["2024-05-01", "ERROR", "timeout"]
Keyword arguments make call sites self-documenting
def connect(host:, port: 443, timeout: 30)
puts "#{host}:#{port} in #{timeout}s"
end
connect(host: "db.internal")Keyword arguments are order-independent, and omitting one raises ArgumentError instead of silently landing in the wrong slot — which is exactly why connect("a", "b") style mistakes get caught at the boundary.
Output
=> db.internal:443 in 30s
Class skeleton with attr_accessor
class User
attr_accessor :name
def initialize(name)
@name = name
end
end
User.new("Tom").nameattr_accessor generates a reader and a writer in one line; attr_reader makes the attribute read-only from outside. The instance variable @name must be assigned in initialize or it reads as nil without any error.
Output
=> "Tom"
Common Pitfalls
- { } binds tighter than do...end, so array.each { puts method } passes method into the block as an argument and raises ArgumentError; use do...end there.
- Array indexing starts at 0, and arr[-1] is the last element — the off-by-one that follows from this contaminates every slice built on it.
- The last expression in a method body becomes the return value, so inserting a puts or a log line mid-method silently changes the result, often to nil.
- A bare rescue catches far more than you intend and can swallow Interrupt, making a script impossible to stop with Ctrl+C.
- proc and lambda check arity differently: lambda demands an exact argument count, so treating one as the other removes a guard rail you may rely on.
- Ruby strings are mutable, so str << "x" edits in place. If the string is a hash key or a constant, mutating it corrupts the key and produces key not found.
Tips
- Everything in Ruby is an object, including nil and numbers.
- each is the most common iterator; map transforms, select filters.
- Symbol :symbol is immutable and unique, good as a hash key.
- attr_accessor auto-generates getter and setter.
- Blocks are Ruby's most powerful feature; do...end and {} are equivalent.
FAQ
Should I reach for map, select or reject — do they differ in return value?
Exactly: map transforms element by element and returns a new array of the same length; select keeps only the elements for which the block is true, so the length changes; reject keeps the ones for which it is false and pairs naturally with select — arr.partition { |x| x.even? } hands you both groups at once as [[evens], [odds]]. Use each when you only want the side effects, since its return value is the original array and map would just waste a copy.
When should a hash key be a symbol rather than a string?
Symbols are immutable and interned — there is exactly one instance per program run — which makes them fast hash keys and the right choice for method names like attr_accessor :name. Strings are mutable and each literal evaluation may allocate anew, so a string key costs extra content hashing. Rule of thumb: symbols for identifiers you write, strings for concatenated values, user input, or anything that has to match parsed JSON. Note that hash.key?(:a) and hash.key?("a") never match each other.
How do Proc and Lambda differ, and what goes wrong if I pick the wrong one?
Both are built from block syntax — Proc.new { |x| x*2 } and ->(x) { x*2 } — but they differ in how strictly they are called. Lambda is strict: the argument count must match its definition, and return exits the method where the lambda was defined. Proc is lenient about arity, and a return inside it raises LocalJumpError. When a callback is invoked by code you do not control, or when you depend on return semantics, the arrow syntax is the safer choice.
How do I generate accessors for a class, and why not write both methods by hand?
attr_accessor :name generates both the reader and the writer in one line; attr_reader :name gives you only the getter, which is what you want when the field should stay read-only and you do not want callers to bypass validation; attr_writer :name gives only the setter. Hand-writing paired getters and setters is verbose and easy to half-update during a rename. Declared in the class body these become instance methods — to make a module's methods class methods use extend MyModule, since include only mixes in instance methods.
How do I exit early from a loop — break, next, or catch/throw?
break leaves the innermost loop or 3.times { } block immediately; next is the equivalent of continue and just skips to the next iteration. Both only affect one level, so neither helps when you are inside nested loops and need to terminate the outer one. That is what the non-local jump is for: catch(:done) { throw(:done) }, usually combined with break or return, lets a generator-style routine bail out from any depth inside a single block.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us