Blocks, Procs, and Lambdas
Ruby treats chunks of code as first-class values through three closely related mechanisms: blocks, Procs, and lambdas. All three are closures -- they capture the local variables and self from the scope in which they were created -- but they differ in how they're passed around, how strict they are about argument counts, and what return does inside them. A block is the anonymous chunk of code attached to a method call with do...end or curly braces; a Proc is an object wrapping a block of code that can be stored in a variable and passed around; a lambda is a special kind of Proc with method-like argument checking and return behavior. Mastering the distinctions is key to writing idiomatic Ruby, since blocks power nearly every Enumerable method.
Cricket analogy: Blocks, Procs, and lambdas are like three ways a substitute fielder can be briefed: a block is a quick shouted instruction from the captain for one play, a Proc is a written note the twelfth man can carry and reuse across overs, and a lambda is a formal signed instruction sheet with strict rules about what it covers.
Blocks as implicit code arguments
Every Ruby method can optionally receive a block, even if its definition doesn't mention one explicitly. The block isn't a regular argument; it's passed implicitly and captured inside the method with yield or by converting it to a Proc with an explicit &block parameter. Blocks are the most common way Ruby code expresses "do this operation, but let the caller customize a step," which is why each, map, select, and friends all read so naturally.
Cricket analogy: Just as a coach's shouted field-placement instruction is implicitly understood without being written into the team sheet, a Ruby method can implicitly accept a block without declaring it, capturing it with yield or an explicit &block parameter the way a captain might explicitly note a specific fielding plan.
[1, 2, 3].each { |n| puts n * 2 }
[1, 2, 3].each do |n|
puts n * 2
end
def repeat(times)
return enum_for(:repeat, times) unless block_given?
times.times { |i| yield i }
end
repeat(3) { |i| puts "iteration #{i}" }Procs: flexible, storable closures
A Proc is created with Proc.new { ... } or the proc { ... } shorthand, and it can be stored in a variable, passed to methods, and called later with .call, .(), or []. Procs are lenient about arity: calling a Proc with too few or too many arguments does not raise an error -- missing arguments become nil and extras are silently discarded, much like how JavaScript functions historically ignored extra arguments.
Cricket analogy: A Proc is like a substitute umpire's written checklist that can be handed off and reused across matches; call it with too few checks (.call) and it silently skips them, call it with extras and it just ignores the rest, much like a lenient scorer overlooking incomplete paperwork.
adder = Proc.new { |a, b| (a || 0) + (b || 0) }
adder.call(1, 2) # => 3
adder.call(1) # => 1 (b is nil, treated as 0)
adder.call(1, 2, 3) # => 3 (extra arg 3 ignored)Lambdas: strict arity and local return
A lambda, created with lambda { ... } or the stabby-lambda syntax ->(a, b) { ... }, behaves much more like a regular method. It enforces strict arity -- calling it with the wrong number of arguments raises an ArgumentError -- and a return inside a lambda only exits the lambda itself, returning control to the calling code, rather than attempting to return from the enclosing method. This local-return behavior is what makes lambdas safe to use deep inside other methods without unexpected control-flow jumps.
Cricket analogy: A lambda is like a strict DRS review protocol: calling it with the wrong number of camera angles raises an error immediately, and a return inside it only ends that single review, handing control straight back to the umpire rather than aborting the whole match.
square = ->(n) { n * n }
square.call(4) # => 16
square.(4) # => 16
square[4] # => 16
safe_divide = lambda do |a, b|
return Float::INFINITY if b.zero?
a / b.to_f
end
safe_divide.call(10, 0) # => Infinity, and execution continues normally afterwardA return inside a Proc (not a lambda) attempts to return from the enclosing method that created the Proc, not just the Proc itself. If that enclosing method has already finished executing by the time the Proc is called, Ruby raises a LocalJumpError. This is one of the most confusing gotchas for developers moving from lambdas to Procs, or from other languages where closures never behave this way.
You can convert a block into a Proc using &block in a method signature, and convert a Proc back into a block-like argument at a call site using &my_proc. Symbols also respond to to_proc, which is why ["a", "b"].map(&:upcase) works: &:upcase calls :upcase.to_proc and passes the resulting Proc as a block.
- Blocks are anonymous, implicit code arguments attached with
do...endor{ }; only one can be passed per method call. - Procs are objects wrapping a block; they can be stored, passed around, and are lenient about argument count (arity).
- Lambdas are Procs created via
lambdaor->(); they enforce strict arity and check argument counts like methods do. returninside a lambda exits only the lambda;returninside a Proc attempts to return from the enclosing method.&blockconverts a block to a Proc parameter;&:symbolleveragesSymbol#to_procfor concise transformations.- Blocks/Procs/lambdas all close over local variables and
selffrom their defining scope, making them true closures.
Practice what you learned
1. What happens when you call a lambda with the wrong number of arguments?
2. What does `return` do inside a Proc (not a lambda)?
3. Which syntax creates a lambda in Ruby?
4. What does `&:upcase` do when passed to `.map`?
5. How many blocks can be passed to a single method call in Ruby?
Was this page helpful?
You May Also Like
yield and block_given?
Understand how `yield` invokes an implicit block passed to a method, and how `block_given?` lets methods branch based on whether a block was supplied.
Method Arguments and Defaults
Learn how Ruby methods accept required, optional, splat, keyword, and block arguments, and how default values keep call sites flexible and expressive.
each, map, and select
Master the three most commonly used Enumerable methods — each for side effects, map for transformation, and select for filtering — and when to reach for which.
define_method and Metaprogramming
Discover how define_method, class_eval, and instance_variable_get/set let Ruby programs write code that writes code, powering DSLs, ORMs, and dynamic accessor generation.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics