ZetCode

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.

blocks_basic.rb
# 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_param.rb
# &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_lambda.rb
# 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.rb
# 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_next_break.rb
# 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.rb
# 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.