Ruby Blocks, Procs, and Lambdas
last modified October 18, 2026
Blocks, procs, and lambdas are the callable constructs that make Ruby's iterators
and DSLs possible. A block is a chunk of code passed to a method; a proc and a
lambda are that same code wrapped in an object that can be stored and called
later. This tutorial covers block syntax, yield, the
&block parameter, the differences between Proc.new,
proc, and lambda, arity, how return,
next, and break behave, and closures.
Block Syntax and yield
A block is written either in braces { } or with do and
end. Both forms bind to the method call they follow. Braces are
conventionally used for single-line blocks and do/end for multi-line
ones. Inside a method, yield runs the block, optionally passing
arguments, and block_given? tells you whether one was supplied.
# do/end and brace block syntax
def twice
yield
yield
end
twice { puts "brace block" }
twice do
puts "do/end block"
end
# yield with arguments
def pair
yield 1, 2
end
pair { |a, b| puts "a=#{a}, b=#{b}" }
# block_given?
def maybe
if block_given?
yield
else
puts "no block given"
end
end
maybe
maybe { puts "block given" }
# Block return value
result = [1, 2, 3].map { |x| x * 10 }
p result
twice yields twice, so the brace block and the do/end
block each run two times. pair passes two arguments to the block's
parameters |a, b|. The maybe method falls back to a
default message when no block is passed. The map call returns the
block's value for each element, so the result is a new array.
$ ./blocks_basic.rb brace block brace block do/end block do/end block a=1, b=2 no block given block given [10, 20, 30]
Capturing a Block with &block
Prefixing the last parameter with & converts the block into a
Proc object that you can call, store, or forward. The reverse
operation, & at a call site, passes a Proc or a Symbol as the
block. &:upcase is the common shorthand for
{ |s| s.upcase }.
# &block captures a block as a Proc
def run_with_log(name, &block)
puts "start #{name}"
value = block.call(5)
puts "end #{name}"
value
end
r = run_with_log("job") { |n| n * n }
puts "result: #{r}"
# Passing a Proc to a method with &
def apply_twice(x, &fn)
fn.(fn.(x))
end
inc = proc { |n| n + 1 }
puts apply_twice(3, &inc)
# Symbol to proc shorthand
p %w[ruby blocks].map(&:upcase)
# Inspect block class
def show_class(&b)
b.class
end
p show_class { }
p show_class
run_with_log receives the block as block and calls it
with call. When no block is given, &b is
nil, which is why the last call prints NilClass. Using
&inc at the call site passes an existing Proc as the block.
$ ./block_param.rb start job end job result: 25 5 ["RUBY", "BLOCKS"] Proc NilClass
Proc.new, proc, and lambda
Proc.new and proc create non-lambda procs. They are
lenient about arguments: missing values become nil and extra values
are dropped. lambda and the stabby arrow -> create
lambdas, which check arguments strictly like methods do. The
lambda? method reports which kind you have.
# Proc.new, proc, and lambda
p1 = Proc.new { |x, y| "Proc.new: x=#{x.inspect} y=#{y.inspect}" }
p2 = proc { |x, y| "proc: x=#{x.inspect} y=#{y.inspect}" }
l1 = lambda { |x, y| "lambda: x=#{x} y=#{y}" }
l2 = ->(x, y) { "stabby: x=#{x} y=#{y}" }
puts p1.call(1)
puts p2.call(1, 2, 3)
puts l1.call(1, 2)
puts l2.(4, 5)
puts "lambda? #{p1.lambda?} #{l1.lambda?} #{l2.lambda?}"
# Strict vs lenient arguments
begin
l1.call(1)
rescue ArgumentError => e
puts "ArgumentError: #{e.message}"
end
Proc.new and proc behave identically here: the first
call passes one argument, so y is nil, and the second
call passes three, so the extra value is ignored. The lambda with one argument
raises ArgumentError. The stabby syntax ->(x, y)
is the most common way to write a lambda today.
$ ./proc_lambda.rb Proc.new: x=1 y=nil proc: x=1 y=2 lambda: x=1 y=2 stabby: x=4 y=5 lambda? false true true ArgumentError: wrong number of arguments (given 1, expected 2)
Arity
arity returns the number of required arguments. Optional arguments
and splats make it negative: a value of -n-1 means n
required arguments plus optional or variadic ones. Procs and lambdas also differ
in how they report arity for optional parameters, and
parameters shows the full signature.
# Arity of procs, lambdas, and methods
p1 = proc { |a, b| }
p2 = lambda { |a, b| }
p3 = proc { |*args| }
p4 = lambda { |a, b = 1| }
p5 = proc { }
p6 = lambda { |a, *rest| }
puts "proc {|a,b|}: #{p1.arity}"
puts "lambda {|a,b|}: #{p2.arity}"
puts "proc {|*args|}: #{p3.arity}"
puts "lambda {|a,b=1|}: #{p4.arity}"
puts "proc {}: #{p5.arity}"
puts "lambda {|a,*rest|}: #{p6.arity}"
def sample(a, b = 2); end
puts "method sample: #{method(:sample).arity}"
puts "parameters: #{p4.parameters.inspect}"
A non-lambda proc with only optional or no parameters reports arity from its
required parameters, so proc { |a, b| } is 2 but
proc { } is 0. Variadic and optional parameters produce negative
values, as with lambda { |a, b = 1| } returning -2. The
parameters method exposes each parameter's kind (:req,
:opt, and so on).
$ ./arity.rb
proc {|a,b|}: 2
lambda {|a,b|}: 2
proc {|*args|}: -1
lambda {|a,b=1|}: -2
proc {}: 0
lambda {|a,*rest|}: -2
method sample: -2
parameters: [[:req, :a], [:opt, :b]]
return, next, and break
The three keywords behave differently depending on where they appear. In a proc,
return exits the method in which the proc was defined. In a lambda,
return exits only the lambda itself. next ends the
current block invocation and supplies its value. break ends the
method that yielded to the block and makes that method return the value.
# return inside a proc exits the enclosing method
def proc_return
pr = proc { return :from_proc }
pr.call
:not_reached
end
# return inside a lambda only exits the lambda
def lambda_return
la = lambda { return :from_lambda }
val = la.call
"lambda returned #{val.inspect}, method continues"
end
p proc_return
puts lambda_return
# next exits the current block with a value
doubled = [1, 2, 3, 4].map { |x| next 0 if x.even?; x * 2 }
p doubled
# break exits the method that yielded, returning a value
found = [5, 8, 11].each { |x| break x if x > 6 }
p found
proc_return never reaches :not_reached because the
proc's return unwinds the whole method. lambda_return
keeps running after the lambda returns. In the map call,
next 0 makes the even elements map to 0. The each call
stops at 8 because break terminates the iteration and returns that
value from each.
$ ./return_next_break.rb :from_proc lambda returned :from_lambda, method continues [2, 0, 6, 0] 8
Closures
A closure is a block, proc, or lambda that remembers the local variables visible where it was created. The captured variables are shared by reference, not copied, so the closure sees later changes and can modify them. Each invocation of the method that creates closures produces a fresh set of captured variables.
# Closures capture local variables from their defining scope
def make_counter
count = 0
increment = -> { count += 1 }
get = -> { count }
[increment, get]
end
inc, get = make_counter
3.times { inc.call }
puts "count: #{get.call}"
# Each call to make_counter creates an independent closure
inc2, get2 = make_counter
inc2.call
puts "count2: #{get2.call}, count: #{get.call}"
# Closures see later changes to captured variables
multiplier = 2
times = ->(x) { x * multiplier }
multiplier = 10
puts times.call(5)
# Block closure over a loop variable
adders = [1, 2, 3].map { |n| ->(x) { x + n } }
puts adders.map { |f| f.call(100) }.inspect
increment and get share the same count
variable, so calls to one are visible through the other. A second call to
make_counter gets its own count, which is why
count2 is 1 while the first counter stays at 3. The
times lambda reads multiplier when it is called, so it
sees the value 10. Each element of the map captures its own
n.
$ ./closures.rb count: 3 count2: 1, count: 3 50 [101, 102, 103]
Source References
Learn more from these resources: Proc Documentation, Method Syntax, and Ruby Method Documentation.
List all Ruby tutorials.
Author
My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.