Blocks, Procs and Lambdas

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18). Features newer than Ruby 3.0 name the version that introduced them, and every example was run on Ruby 4.0 with warnings enabled. A block is the chunk of code a method call carries with it; a proc is that code turned into an object; a lambda is a proc with method-like rules; and a closure is any of them together with the variables it can still see. Almost everything idiomatic in Ruby, from each and map to File.open, Struct.new and every DSL, rests on this one mechanism. This page covers the whole of it: the syntax of blocks and their parameters, the numbered parameters and the it parameter, the exact differences between procs and lambdas (arity, return, break and next), closures and Binding, Method and UnboundMethod objects, to_proc and &, currying and composition, and callable objects as a design tool.

It builds on Methods and Parameters (the &block parameter and the other parameter kinds) and Calling Methods and Argument Forwarding (passing blocks with &, anonymous forwarding). Later pages rely on it: Enumerable, Enumerators and Lazy Evaluation, Dynamic Dispatch and Method Definition and Hooks, eval and DSLs. How scope rules decide which variables a block can see is in Variables, Constants and Scope, and break, next and redo as statements are in Control Expressions. For other languages' approaches, see Go closures, Rust closures, Swift closures, Kotlin lambdas, Groovy closures, Java lambdas, C# delegates and lambdas and PHP closures.

Examples that use the shelf scenario (the small library catalogue gem used throughout this section) call require "shelf" (see the shelf gem source); the others are self-contained. Output is the real output of the code as shown, with # => marking the value of the line above (using the Ruby 3.4+ inspect format) and # prints: marking what puts writes.

The three objects at a glance

Block Proc Lambda

What it is

Syntax attached to one method call; not an object

An instance of Proc with proc semantics (lambda? is false)

An instance of Proc with method semantics (lambda? is true)

Created by

do ... end or braces after a call

Proc.new, proc, or &block in a parameter list

->(x) { }, lambda { }

Called by

yield inside the method it was given to

call, .(), [], yield, ===

the same five forms

Wrong number of arguments

Missing ones are nil, extras are dropped

Missing ones are nil, extras are dropped

ArgumentError

return

Returns from the method that wrote the block

Returns from the method that wrote the proc (LocalJumpError if it already returned)

Returns from the lambda only

break

Ends the method the block was given to

LocalJumpError once that method has returned

Returns from the lambda only

next

Ends this call of the block, giving a value

Ends this call of the proc, giving a value

Ends this call of the lambda, giving a value

The rest of the page justifies every cell of this table.

Blocks

Syntax: braces versus do and end

A block is written after the arguments of a method call, either between braces or between do and end. Both forms mean the same thing except for precedence: braces bind to the nearest call (the one just to their left), while do ... end binds to the whole command. The difference only shows when a call has no parentheses, because then there are two calls the block could belong to. The precedence table in the language reference lists { ... } blocks above do ... end blocks, and the calling-methods reference states the same rule.

# frozen_string_literal: true

titles = %w[dune emma ulysses]

# Braces bind to the nearest call, do...end to the whole command.
braces = titles.map { |title| "#{title}!" }
p braces
# => ["dune!", "emma!", "ulysses!"]

p(titles.map { |title| title.length * 2 }.sum)
# => 30

# rubocop:disable Lint/AmbiguousBlockAssociation
# With do...end the block goes to p, not to map, so map returns an Enumerator.
p titles.map do |title|
  "#{title}?"
end
# => #<Enumerator: ["dune", "emma", "ulysses"]:map>
# rubocop:enable Lint/AmbiguousBlockAssociation

# Assigning first keeps the block on map.
questions = titles.map do |title|
  "#{title}?"
end
p questions
# => ["dune?", "emma?", "ulysses?"]

The convention that follows from this (and the default of RuboCop’s Style/BlockDelimiters) is braces for a one-line block whose value is used, do ... end for a multi-line block or one that is called for its side effects. It is a convention for readability, not a rule of the language; the real constraint is that a chained call after the block (map { ... }.sum) needs the braces, or a parenthesised do ... end.

A method call has at most one block. The block is always the last thing in the call, and it is not an argument in the ordinary sense: it is not part of the positional or keyword arguments, it is not counted by arity, and it is passed on only explicitly (with &) or implicitly (by super). A block can be attached to any method call, including ones that never use it; Ruby does not complain, which is why puts [1, 2].map do |n| ... end silently hands the block to puts.

yield, block_given? and the value of a block

Inside a method, yield runs the block that the method was called with, passing it the arguments given to yield and returning the value of the block’s last expression. block_given? reports whether there is a block. Calling yield when there is none raises LocalJumpError ("no block given (yield)").

# frozen_string_literal: true

# A method runs its block with yield and reports whether it was given one.
def audited(action)
  return "no block for #{action}" unless block_given?

  puts "start #{action}"
  result = yield
  puts "finish #{action}"
  result
ensure
  puts "audit closed"
end

p audited("lend") { 42 }
# prints: start lend
# prints: finish lend
# prints: audit closed
# => 42
p audited("lend")
# prints: audit closed
# => "no block for lend"

# yield passes arguments to the block and returns the block's value.
def each_numbered(items)
  return enum_for(__method__, items) unless block_given?

  items.each_with_index { |item, index| yield item, index + 1 }
end

each_numbered(%w[Dune Emma]) { |title, number| puts "#{number}. #{title}" }
# prints: 1. Dune
# prints: 2. Emma
p each_numbered(%w[Dune Emma]).to_a
# => [["Dune", 1], ["Emma", 2]]

def without_block = yield

begin
  without_block
rescue LocalJumpError => e
  puts "#{e.class}: #{e.message}"
end
# prints: LocalJumpError: no block given (yield)

Points worth knowing:

  • The return enum_for(__method__, ...) unless block_given? line is the standard way to let a custom iterator work both with a block and as an Enumerator (each_numbered(...).to_a, .next, .with_index, lazy chains). The shelf Catalog#each uses the same idiom (see The shelf Catalog in practice). __method__ names the current method, which survives renaming.

  • The ensure clause of a method runs when the block exits the method early (by break, return, throw or an exception), so cleanup around a yield is reliable. This is the shape of File.open with a block, Mutex#synchronize and Dir.chdir with a block.

  • Whatever the block returns is the value of the yield expression, so a method can let its caller customise a decision: items.sum { |item| item.weight }, array.sort_by { ... }, hash.fetch(key) { default }.

  • A block gets the arguments of yield with block parameter semantics (below): yield a, b and yield [a, b] look alike to a |x, y| block but differ for |x|.

  • yield is also the cheapest way to call a block: it needs no Proc object. The language reference says to prefer a bare yield when you only call the block and do not store or forward it.

The &block parameter and anonymous forwarding

To store the block, return it or pass it on, give the method a parameter prefixed with &. Ruby converts the block into a Proc and binds it to that name; the parameter is nil when no block was given. Since Ruby 3.1 the block parameter can be anonymous (a bare &), which can then only be forwarded: def each(&) = @items.each(&). At the call site, &expression does the reverse: it converts the value to a proc (by calling to_proc on it, unless it is already a Proc) and passes it as the block. &nil passes no block.

# frozen_string_literal: true

# &block turns the block into a Proc; a bare & forwards it without a name (3.1).
def explicit(&block) = block
def forward(&) = explicit(&)

block = explicit { |title, copies = 1| "#{title} x#{copies}" }
p block.class, block.lambda?, block.arity
# => Proc
# => false
# => 1
p block.call("Dune")
# => "Dune x1"
p forward { 1 }.class
# => Proc
p explicit
# => nil

# Passing an existing Proc keeps its lambda-ness.
strict = ->(title) { "#{title}!" }
p explicit(&strict).lambda?
# => true
p(%w[a b].map(&strict))
# => ["a!", "b!"]

Rules for the conversion:

  • A Proc passed with & stays the same object, keeps its lambda-ness and is not wrapped. The same Proc object comes out of &block that went in, which is why lambdas passed as blocks keep their strict arity.

  • A Symbol, Method or Hash becomes a lambda-like proc through its own to_proc (see to_proc and the ampersand).

  • Anything else raises TypeError ("no implicit conversion of Integer into Proc"; the message names the class).

  • The anonymous & can be forwarded from the method body, from a nested block or from a nested proc (all three work in Ruby 4.0), but only inside the method that declared it; name the parameter when more than one level needs it.

  • The ... forwarding parameter includes the block, so def wrapper(...) = target(...) forwards it too.

Block parameters

A block’s parameter list sits between vertical bars. It accepts every kind of parameter a method does (required, optional, splat, keyword, keyword splat and &block), and also destructuring parentheses, but follows the lenient proc rules for arguments:

  • Missing arguments are nil; extra arguments are dropped.

  • If the block has more than one parameter (or a trailing comma, or a splat together with other parameters) and receives exactly one argument that is an Array, the array is spread over the parameters. This is why hash.each { |key, value| } works although Hash#each yields one [key, value] pair, and why |a| receives the whole pair.

  • Parentheses destructure a nested array: |a, (b, c)|. They nest and combine with splats: |a, (b, *c), d|.

  • A trailing comma (|a, |) takes the first element and ignores the rest, the same as |a, *|.

  • Keyword parameters are filled from a trailing hash that yield passes as keywords (yield(key: 1) to |key:|). Since Ruby 3.2 a proc that takes exactly one positional parameter plus keywords no longer auto-splats an array argument.

# frozen_string_literal: true

# Procs and blocks are lenient: missing arguments are nil, extras are dropped.
pair = proc { |first, second| [first, second] }
p pair.call(1)
# => [1, nil]
p pair.call(1, 2, 3)
# => [1, 2]
p pair.call([1, 2])
# => [1, 2]

# Destructuring, splats, defaults, keywords and a block-local variable.
rows = [["Dune", %w[Frank Herbert], 1965]]
rows.each do |title, (first, last), year = 0, *rest, edition: 1|
  p [title, first, last, year, rest, edition]
end
# => ["Dune", "Frank", "Herbert", 1965, [], 1]

[[1, [2, 3]], [4, [5, 6]]].each { |a, (b, c)| puts a + b + c }
# prints: 6
# prints: 15

{ "Dune" => 1965 }.each { |title, year| puts "#{title} (#{year})" }
# prints: Dune (1965)
p({ "Dune" => 1965 }.map { |entry| entry })
# => [["Dune", 1965]]

total = 100
[1, 2].each { |n; total| total = n } # rubocop:disable Lint/UselessAssignment
p total
# => 100
[1, 2].each { |n| total = n }
p total
# => 2

The auto-splat deserves care, because it is the main way a block differs from a lambda (see below) and from a method. [[1, 2]].each { |a, b| } gives a = 1, b = 2; [[1, 2]].each { |a| } gives a = [1, 2]; [[1, 2]].each { |a, | } gives a = 1. With each_with_index or each_with_object, the pair is (element, index), so destructuring the element needs parentheses: |(a, b), index|.

Block-local variables and scope

A block is a closure scope, not a scope gate: it can read and assign the variables of the code around it, while def, class and module start a fresh scope that cannot. Variables first assigned inside the block are local to that call of the block and invisible after it (NameError: "undefined local variable or method 'y' for main"). A parameter with the same name as an outer variable shadows it for the block and leaves the outer variable untouched. To make a variable block-local without making it a parameter, list it after a semicolon: |n; total|. The example above shows both the shadowing form and the leaking form.

Since Ruby 2.6 there is no warning for shadowing an outer variable, so only a linter can tell you (RuboCop’s Lint/ShadowingOuterLocalVariable exists but is disabled by default). The block-local list is rarely needed; if a block needs a private temporary and the outer scope has the same name, renaming the temporary is almost always clearer.

Numbered parameters and the it parameter

For one-line blocks, Ruby lets you skip the parameter list:

  • _1, _2 … _9 (numbered parameters, Ruby 2.7) name the first, second, … parameter. The highest number used decides the number of parameters, so a block that uses _2 takes two.

  • it (Ruby 3.4) names the single parameter of a block that has none.

They follow these rules:

  • A block that uses _1 alone behaves like |x| (it receives the whole element, so [[1, 2]].map { _1 } is [[1, 2]]). A block that uses _1 and _2 behaves like |a, b| and destructures.

  • it behaves like _1 used alone: one parameter, no auto-splat. In a lambda it makes the lambda take exactly one argument.

  • You cannot combine an ordinary parameter list with _1 or it, you cannot mix _1 with it, you cannot assign to _1, and you cannot use _1 in a block nested inside a block that already uses it. Each is a SyntaxError: the messages are quoted in the table below.

  • it is a soft keyword, not a reserved word. A bare it in a block without parameters is the implicit parameter, unless a local variable named it is visible (then it is that variable: it = 5; [1, 2].map { it } is [5, 5]). it with arguments or a block, or written it() or self.it, stays an ordinary method call, so RSpec’s it "does x" do ... end is unaffected. Nested blocks that each use it refer to their own parameter.

  • Proc#parameters reports for a proc using it and for a lambda (Ruby 4.0 prints [:opt]; Ruby 3.4 printed [:opt, nil]), and for numbered parameters in a lambda. Their arity is the number of parameters.

  • Use them only where the meaning is obvious; the documentation advises limiting yourself to _1 and _2 in one-line blocks, and RuboCop’s Style/NumberedParametersLimit caps it at one.

# frozen_string_literal: true

titles = %w[Dune Emma]

p(titles.map { _1.center(8, "*") }) # rubocop:disable Style/ItBlockParameter
# => ["**Dune**", "**Emma**"]
p(titles.map { it.center(8, "*") })
# => ["**Dune**", "**Emma**"]

# _1 alone behaves like |x|; _1 with _2 destructures like |a, b|.
p([[1, 2]].map { _1 })
# => [[1, 2]]
p([[1, 2]].map { it })
# => [[1, 2]]
p([[1, 2]].map { [_1, _2] }) # rubocop:disable Style/NumberedParametersLimit
# => [[1, 2]]

# The block's parameters as Ruby reports them.
p proc { it }.parameters
# => [[:opt]]
p(-> { it }.parameters)
# => [[:req]]
p(-> { [_1, _2] }.parameters) # rubocop:disable Style/NumberedParametersLimit
# => [[:req, :_1], [:req, :_2]]
p proc { [_1, _2] }.arity # rubocop:disable Style/NumberedParametersLimit
# => 2

# Both reach the block through Binding (4.0).
[10].each { p [it, binding.implicit_parameters, binding.implicit_parameter_defined?(:_1)] }
# => [10, [:it], false]

The syntax errors, copied from running each snippet (Prism, the default parser since Ruby 3.4):

Code Message

[1].each { |x| _1 }

numbered parameters are not allowed when an ordinary parameter is defined

[1].each { |x| it }

'it' is not allowed when an ordinary parameter is defined

[1].each { _1 + it }

'it' is not allowed when a numbered parameter is already used

[1].each { _1; [2].each { _1 } }

numbered parameter is already used in outer block

_1 = 1

_1 is reserved for numbered parameters

Ruby 4.0 changed how these hidden parameters can be read. Binding#local_variables no longer lists numbered parameters (and local_variable_get, local_variable_set and local_variable_defined? refuse them), and three new methods, Binding#implicit_parameters, implicit_parameter_get and implicit_parameter_defined?, report them instead. The last line of the example uses them. Debugging tools and binding.irb are the main users.

Blocks and break, next, redo

Inside a block, next ends the current call of the block and gives its argument (default nil) as the block’s value: [1, 2, 3].map { |n| next 0 if n == 2; n } is [1, 0, 3]. break ends the method the block was given to and gives its argument as that method’s value ([1, 2, 3].each { |n| break n * 10 if n == 2 } is 20). return leaves the method in which the block was written. redo restarts the current call of the block with the same arguments. For escaping several levels of nested blocks at once, use catch and throw, covered with the exception model. The next section shows the same three keywords in procs and lambdas, where the answers differ.

Procs and lambdas

Creating them

A Proc is an object holding a block of code and the variables it was created with. There are four spellings:

  • Proc.new { |x| ... } and proc { |x| ... } build a proc with proc semantics (lambda? is false). proc is the shorthand RuboCop prefers (Style/Proc).

  • lambda { |x| ... } and the stabby literal ->(x) { ... } build a proc with lambda semantics (lambda? is true). RuboCop prefers the literal for one-line lambdas and lambda with do ... end for multi-line ones (Style/Lambda). The stabby parameter list may omit the parentheses (-> x { x }), may be omitted entirely for numbered or it parameters (-> { it * 2 }), and can take a do ... end body (->(x) do ... end).

A callable is invoked with any of five equivalent spellings: call, .(), [], yield and ===. The last is what makes lambdas usable in case/when and in pattern matching (see Callable objects as interfaces).

# frozen_string_literal: true

# rubocop:disable Style/Proc, Style/Lambda, Style/LambdaCall, Style/CaseEquality, Style/SymbolProc
# This file lists every spelling on purpose, including the ones RuboCop rewrites.

# Four ways to build a callable object from a block of code.
from_new = Proc.new { |title| title.upcase }
from_proc = proc { |title| title.upcase }
from_lambda = lambda { |title| title.upcase }
stabby = ->(title) { title.upcase }

[from_new, from_proc, from_lambda, stabby].each { |callable| p callable.lambda? }
# => false
# => false
# => true
# => true

# Five ways to call one.
p stabby.call("dune")
# => "DUNE"
p stabby.("dune")
# => "DUNE"
p stabby["dune"]
# => "DUNE"
p stabby.yield("dune")
# => "DUNE"
p(stabby === "dune")
# => "DUNE"

# Proc.new and proc need a block; lambda needs a literal one.
[-> { Proc.new }, -> { proc }, -> { lambda }, -> { lambda(&from_proc) }].each do |attempt|
  attempt.call
rescue ArgumentError => e
  puts "ArgumentError: #{e.message}"
end
# prints: ArgumentError: tried to create Proc object without a block
# prints: ArgumentError: tried to create Proc object without a block
# prints: ArgumentError: tried to create Proc object without a block
# prints: ArgumentError: the lambda method requires a literal block

# rubocop:enable Style/Proc, Style/Lambda, Style/LambdaCall, Style/CaseEquality, Style/SymbolProc

History that explains old code:

  • Before Ruby 3.0, Proc.new and proc without a block quietly used the block of the surrounding method (2.7 warned). Since 3.0 both raise ArgumentError ("tried to create Proc object without a block"). Legacy code relying on it must pass &block explicitly.

  • lambda(&some_proc) used to return the proc unchanged, with a warning from 3.0. Since Ruby 3.3 it raises ArgumentError ("the lambda method requires a literal block"). lambda(&some_lambda) is accepted and keeps the lambda. (The language reference page for Proc still shows the old behaviour in its examples.)

  • proc and Proc.new given a lambda via & return that lambda, lambda-ness intact. A proc cannot be turned into a lambda; wrap it instead (->(*args) { some_proc.call(*args) }).

  • define_method always creates a method with lambda-like argument checking, even from a non-lambda proc, and Method#to_proc always gives a lambda. See instance_exec and define_method.

Procs versus lambdas

lambda? tells the two apart, and the language reference calls the differences "tricks": a proc has them and a lambda does not, so a lambda behaves like an anonymous method. There are exactly two differences, plus a few consequences of them.

Arguments. A lambda checks its arguments like a method: too few or too many raise ArgumentError ("wrong number of arguments (given 1, expected 2)"), and there is no auto-splat, so strict.call([1, 2]) passes one argument. A proc fills missing ones with nil, drops extras and auto-splats a single array. This also decides what happens when a lambda is passed as a block: [[1, 2]].each(&->(a, b) { }) raises ArgumentError, because each yields one array. Since Ruby 3.0, { a: 1 }.each(&->(key, value) { }) raises too, since Hash#each yields one [key, value] pair; Hash#map happens to accept a two-parameter lambda, which makes the difference easy to trip over; a lambda taking one parameter (->(pair) { }) always works.

Control flow. return and break inside a lambda leave the lambda and give it its value. Inside a proc, return leaves the method that created the proc and break leaves the method that the block was given to, which is why procs can be orphaned (next section). next is the same everywhere: it ends the current call and gives its value.

# frozen_string_literal: true

# rubocop:disable Style/Lambda
# Arity: a proc forgives, a lambda insists.
sloppy = proc { |first, second| [first, second] }
strict = lambda { |first, second| [first, second] }

p sloppy.call(1)
# => [1, nil]
p sloppy.call([1, 2])
# => [1, 2]
begin
  strict.call(1)
rescue ArgumentError => e
  puts "ArgumentError: #{e.message}"
end
begin
  strict.call([1, 2])
rescue ArgumentError => e
  puts "ArgumentError: #{e.message}"
end
# prints: ArgumentError: wrong number of arguments (given 1, expected 2)
# prints: ArgumentError: wrong number of arguments (given 1, expected 2)

p sloppy.arity, strict.arity
# => 2
# => 2
p proc { |first, second = 1| }.arity, lambda { |first, second = 1| }.arity
# => 1
# => -2
p proc { |*all| }.arity, lambda { |*all| }.arity
# => -1
# => -1
p sloppy.parameters, strict.parameters
# => [[:opt, :first], [:opt, :second]]
# => [[:req, :first], [:req, :second]]
p sloppy.parameters(lambda: true)
# => [[:req, :first], [:req, :second]]
# rubocop:enable Style/Lambda

# return, next and break: where do they go?
def proc_return
  proc { return :from_proc }.call
  :after
end

def lambda_return
  -> { return :from_lambda }.call # rubocop:disable Style/RedundantReturn
  :after
end

def block_return = [1, 2, 3].each { |n| return n * 10 if n == 2 }

p proc_return, lambda_return, block_return
# => :from_proc
# => :after
# => 20

p proc { next 7 }.call, -> { next 7 }.call
# => 7
# => 7
p([1, 2, 3].map { |n| n == 2 ? next : n })
# => [1, nil, 3]

def block_break = [1, 2, 3].each { |n| break n * 10 if n == 2 }

p block_break
# => 20
p(-> { break 5 }.call)
# => 5

The diagram summarises where execution continues after each keyword. "Block or proc" means the code between braces or do and end given to a method such as each, or a Proc made by proc or Proc.new; "lambda" is any lambda.

flowchart LR BP["Block or proc"] L["Lambda"] BP -->|return| R1["Leaves the method that wrote the block
LocalJumpError if that method has returned"] BP -->|next| N1["Ends this call of the block
the argument is the block value"] BP -->|break| B1["Ends the method the block was given to
LocalJumpError if that method has returned"] L -->|return| R2["Ends the lambda
the argument is its value"] L -->|next| N2["Ends the lambda
the argument is its value"] L -->|break| B2["Ends the lambda
the argument is its value"]

Orphaned procs and LocalJumpError

A proc’s return and break need their target method to still be running. If a proc outlives the method that created it, or the method it was given to, the jump has nowhere to go and Ruby raises LocalJumpError. A lambda keeps both local, so lambdas cannot be orphaned.

# frozen_string_literal: true

def make_proc = proc { return 1 }
def make_break_proc = proc { break 5 }

[-> { make_proc.call }, -> { make_break_proc.call }].each do |attempt|
  attempt.call
rescue LocalJumpError => e
  puts "LocalJumpError (#{e.reason}): #{e.message}"
end
# prints: LocalJumpError (return): unexpected return
# prints: LocalJumpError (break): break from proc-closure

# A lambda keeps return local, even when it outlives the method that built it.
def make_lambda = -> { return 1 } # rubocop:disable Style/RedundantReturn

p make_lambda.call
# => 1

Practical consequences:

  • Do not store a proc that contains return and call it later. Use a lambda, or next.

  • Storing a block as a callback (@callback = block) and calling it later is fine, but a return or break in it fails the same way. Callback blocks should use next for early exit.

  • A return in a proc that runs in another thread raises LocalJumpError ("unexpected return"), because the target method is not on that thread’s stack; the thread also prints the usual "terminated with exception" report.

  • LocalJumpError is also what yield raises when there is no block, with reason :noreason. It is a StandardError, so a bare rescue catches it; see Exceptions and Error Handling for the hierarchy.

  • return at the top level of a script, even inside a block or proc, simply ends the script (a top-level return is allowed since Ruby 2.4); this surprises people who paste a snippet into a file.

arity, parameters and source_location

Proc#arity returns the number of required arguments, or -n-1 when there are n required arguments and optional ones. It differs between procs and lambdas for optional parameters: proc { |a, b = 1| }.arity is 1 while the same lambda is -2, and lambda { |a:| }.arity is 1 (a required keyword counts as one argument) while lambda { |a: 1| }.arity is -1. Proc#parameters returns the list of [kind, name] pairs, where kind is :req, :opt, :rest, :keyreq, :key, :keyrest or :block; a proc reports its positional parameters as :opt because it does not require them, and parameters(lambda: true) shows what the lambda reading would be. source_location gives [file, line] and Proc#inspect includes it and a (lambda) marker, which is the quickest way to find where an anonymous callable came from. Proc#ruby2_keywords exists only for backward compatibility with code that forwards keywords through *args; the argument-forwarding page explains why ... replaces it.

Closures

A proc remembers the variables of the scope where it was written, not copies of their values. Several procs created in the same scope share the same variables, a proc created in a method keeps that method’s locals alive after the method returns, and a later assignment to a captured variable is visible to the proc.

# frozen_string_literal: true

# Each call to counter closes over its own variable.
def counter(start = 0)
  count = start
  increment = -> { count += 1 }
  peek = -> { count }
  [increment, peek]
end

increment, peek = counter(10)
increment.call
increment.call
p peek.call
# => 12
other_increment, other_peek = counter
other_increment.call
p other_peek.call, peek.call
# => 1
# => 12

# The closure's binding is an object you can inspect and even change.
limit = 3
check = ->(n) { n <= limit }
scope = check.binding
p scope.local_variable_defined?(:limit)
# => true
p scope.local_variable_get(:limit)
# => 3
scope.local_variable_set(:limit, 1)
p check.call(2)
# => false
p scope.receiver
# => main

# for shares one variable between iterations; each gives every pass its own.
shared = []
for i in 1..3 do shared << -> { i } end # rubocop:disable Style/For
p shared.map(&:call)
# => [3, 3, 3]
separate = (1..3).map { |n| -> { n } }
p separate.map(&:call)
# => [1, 2, 3]

# A proc remembers where it came from.
p(check.source_location.then { |file, line| [File.basename(file), line] })
# => ["ex09_closures.rb", 24]
p check.inspect.sub(/0x\h+/, "0x...").sub(/ \S+:/, " FILE:")
# => "#<Proc:0x... FILE:24 (lambda)>"

# A closure captures variables, not values.
rate = 2
double = ->(n) { n * rate }
rate = 3
p double.call(5)
# => 15

What follows from this:

  • Factory functions: counter returns two lambdas over the same count, a lightweight alternative to a class with one instance variable. A lambda closing over mutable state is an object in disguise; when the state grows, switch to a class.

  • Binding: Proc#binding returns the captured scope as a Binding object, with local_variable_get, local_variable_set, local_variable_defined?, local_variables, receiver and eval. Kernel#binding captures the current scope. The mechanism behind ERB templates, binding.irb and many debuggers. Writing through a Binding changes the variable the closure sees, as the example shows.

  • The for loop does not make a scope: its variable is shared by all iterations (all three lambdas return 3), while a block parameter is fresh for each call. This is the classic closure bug; prefer each, map, times or upto.

  • Memory: a closure keeps the whole captured scope reachable, not just the variables it uses. A long-lived callback created in a method with a big local (a loaded file, a large array) keeps that object alive. Create long-lived lambdas in small methods, or assign big locals to nil when done. See Garbage Collection and Memory.

  • Threads: threads started with Thread.new { ... } share the captured variables, so sharing mutable captured state needs a Mutex; see Threads and the GVL. A block passed to Ractor.new cannot access outer variables at all; Ruby 4.0 adds Ractor.shareable_proc and Ractor.shareable_lambda to make a proc that can cross a Ractor boundary (see Ractors).

  • Constants and instance variables: a block sees self, instance variables, constants and methods of where it was written, because self is part of the captured context. The next section shows how to change that on purpose.

Blocks that change self: instance_exec and define_method

A block normally keeps the self it was written in. instance_eval, instance_exec, class_eval, class_exec and define_method run a block with a different self, while the captured local variables stay visible. This is the basis of nearly every Ruby DSL (RSpec, Rake, Bundler’s Gemfile, the Shelf.configure style): the block reads like configuration, but runs against an object that interprets each call.

# frozen_string_literal: true

require "shelf"

# A block normally keeps the self it was written in; instance_exec rebinds it.
class Report
  def initialize = @lines = []

  def title(text) = @lines << text.upcase

  def row(text) = @lines << "- #{text}"

  def build(&)
    instance_exec(&)
    @lines
  end
end

prefix = "Shelf"
lines = Report.new.build do
  title "#{prefix} report"
  row "Dune"
end
p lines
# => ["SHELF REPORT", "- Dune"]

# Arguments are passed through, and self is the receiver while locals stay visible.
p "dune".instance_exec(2) { |times| self * times }
# => "dunedune"

define_method is the closure-based way to define a method: because the body is a block, it can use the local variables around the definition (a loop variable, a configuration value), which a def body never can.

# frozen_string_literal: true

# define_method closes over the surrounding scope, which def never does.
class Loan
  %i[available lent overdue returned].each do |state|
    define_method(:"#{state}?") { @state == state }
  end

  define_method(:renew) { |days| "renewed for #{days} days" }

  def initialize(state) = @state = state
end

p Loan.new(:lent).lent?, Loan.new(:lent).returned?
# => true
# => false

# A block given to define_method is a lambda-like method body, so arity is checked.
begin
  Loan.new(:lent).renew
rescue ArgumentError => e
  puts "ArgumentError: #{e.message}"
end
# prints: ArgumentError: wrong number of arguments (given 0, expected 1)

Details to remember:

  • instance_exec(*args, &block) passes arguments to the block; instance_eval passes the receiver as the only argument. Prefer instance_exec.

  • yield inside a define_method body is a SyntaxError ("Invalid yield") when written at class level, and refers to the enclosing method’s block when written inside another method. A method defined with define_method receives its own block through an explicit parameter: define_method(:each) { |&block| ... }.

  • A method defined by define_method can return with return, because a method defined from a block has lambda semantics.

  • Constants are looked up where the block was written, not in the new self (Outer.module_eval { X } does not find Outer::X), while def inside the block defines instance methods under class_eval and singleton methods under instance_eval. The rules are on Hooks, eval and DSLs and Dynamic Dispatch and Method Definition.

Blocks and super

A method that calls super passes its block to the superclass method automatically, whichever form it uses (bare super, super() or super(a, b)). Only an explicit &nil (super(&nil)) suppresses it, and super(&other) replaces it.

# frozen_string_literal: true

# rubocop:disable Style/OneClassPerFile, Lint/UselessMethodDefinition
# Zsuper (bare super) passes the block along; super() with parentheses does too.
class Base
  def each_item = yield(:base)
end

class Child < Base
  def each_item
    super
  end
end

Child.new.each_item { |item| p item }
# => :base

class Silent < Base
  def each_item
    super(&nil)
  end
end

begin
  Silent.new.each_item { |item| p item }
rescue LocalJumpError => e
  puts "LocalJumpError: #{e.message}"
end
# prints: LocalJumpError: no block given (yield)

# rubocop:enable Style/OneClassPerFile, Lint/UselessMethodDefinition

Source: Methods

(The super rules for arguments and for method lookup are in Method Lookup and super.)

Method and UnboundMethod

obj.method(:name) returns a Method, a callable that remembers its receiver; Klass.instance_method(:name) returns an UnboundMethod, the same without a receiver. They are the object form of a method, not a closure over a block: call, to_proc, arity, parameters, owner, receiver, name, original_name (the name before alias), source_location, super_method, curry, >>, << and unbind / bind / bind_call cover what you can do with them.

# frozen_string_literal: true

require "shelf"

book = Shelf::Book.new(
  isbn: Shelf::Isbn.new(digits: "978-0-441-17271-9"),
  title: "Dune",
  author: Shelf::Author.new(name: "Frank Herbert")
)

# A Method is a method bound to a receiver.
to_string = book.method(:to_s)
p to_string.call
# => "Dune by Frank Herbert"
p to_string.receiver == book, to_string.owner, to_string.name, to_string.arity
# => true
# => Shelf::Book
# => :to_s
# => 0
p book.method(:title).owner, book.method(:with).owner
# => Shelf::Book
# => Data
p [1, 2, 3].method(:inject).arity, Integer.instance_method(:+).parameters
# => -1
# => [[:req]]

# An UnboundMethod has an owner but no receiver; bind it to any compatible object.
unbound = Shelf::Book.instance_method(:to_s)
p unbound.class, unbound.bind(book).call == to_string.call
# => UnboundMethod
# => true
p unbound.bind_call(book)
# => "Dune by Frank Herbert"
p to_string.unbind.name
# => :to_s

begin
  unbound.bind(Object.new)
rescue TypeError => e
  puts "TypeError: #{e.message}"
end
# prints: TypeError: bind argument must be an instance of Shelf::Book

begin
  book.method(:borrow)
rescue NameError => e
  puts "NameError: #{e.message}"
end
# prints: NameError: undefined method 'borrow' for class 'Shelf::Book'

# A Method converts to a lambda, so it can stand in for a block.
p [book].map(&:to_s)
# => ["Dune by Frank Herbert"]
p book.method(:to_s).to_proc.lambda?
# => true
titles = %w[Dune Emma].map(&Shelf::Author.method(:new))
p titles
# => [#<data Shelf::Author name="Dune">, #<data Shelf::Author name="Emma">]
p(book.method(:to_s).source_location.then { |file, line| [File.basename(file), line] })
# => ["book.rb", 5]

Notes:

  • method(:name) finds public, protected and private methods; public_method only public ones. A name that does not exist raises NameError ("undefined method 'borrow' for class 'Shelf::Book'"), unless respond_to_missing? says the object handles it (then the Method calls method_missing).

  • A Method converts to a lambda (to_proc.lambda? is true), so it has strict arity: [[1, 2]].each(&method(:foo)) with def foo(a, b) raises ArgumentError.

  • bind accepts only an instance of the owner’s class (or, for a method from a module, any object); anything else raises TypeError ("bind argument must be an instance of Shelf::Book"). bind_call binds and calls in one step without allocating a Method object, which is why it is the idiom for calling a method as defined in a specific class even when the receiver overrides it (Kernel.instance_method(:class).bind_call(obj)).

  • Method#inspect prints the owner, the receiver class in parentheses when they differ, the parameters and the source location: #<Method: Object(Kernel)#puts(*)>.

  • Method#owner for an inherited or mixed-in method is the ancestor that defines it (Data for with), which makes method an introspection tool; see Reflection, Introspection and Tracing.

  • Methods with unusual parameters report them in parameters: [[:req, :a], [:opt, :b], [:rest, :c], [:keyreq, :d], [:key, :e], [:keyrest, :f], [:block, :g]]. The arity of such a method is -3.

to_proc and the ampersand

The & in a call asks the argument for its to_proc. This single protocol is why &:upcase, &method(:puts) and &hash work as blocks, and why you can make your own objects usable the same way.

# frozen_string_literal: true

require "shelf"

# Symbol#to_proc calls the method on its first argument.
upcase = :upcase.to_proc
p upcase.call("dune"), upcase.lambda?, upcase.arity, upcase.parameters
# => "DUNE"
# => true
# => -2
# => [[:req], [:rest]]
p [[1, 2], [3, 4]].map(&:sum)
# => [3, 7]
p [1, 2, 3].inject(&:+)
# => 6

# The receiver is the first yielded value; the rest become arguments.
begin
  %w[a b].each_with_index.map(&:first)
rescue NoMethodError => e
  puts "NoMethodError: #{e.message}"
end
# prints: NoMethodError: undefined method 'first' for an instance of String

begin
  upcase.call
rescue ArgumentError => e
  puts "ArgumentError: #{e.message}"
end
# prints: ArgumentError: no receiver given

# Hash#to_proc and Method#to_proc turn lookups and methods into blocks.
authors = { "Dune" => "Frank Herbert", "Emma" => "Jane Austen" }
p %w[Dune Emma Ulysses].filter_map(&authors)
# => ["Frank Herbert", "Jane Austen"]
p %w[1 2].map(&method(:Integer))
# => [1, 2]

# Any object with to_proc works after &.
class Prefixed
  def initialize(prefix) = @prefix = prefix

  def to_proc = ->(title) { "#{@prefix}#{title}" }
end

p %w[Dune Emma].map(&Prefixed.new("> "))
# => ["> Dune", "> Emma"]

begin
  %w[Dune].map(&Object.new)
rescue TypeError => e
  puts "TypeError: #{e.message}"
end
# prints: TypeError: no implicit conversion of Object into Proc
  • Symbol#to_proc builds a lambda that calls the named method on its first argument with the remaining arguments (:upcase.to_proc.arity is -2). That is why &:sym fails when a block receives several values and the first one is not the receiver you meant (the each_with_index.map(&:first) case above).

  • The symbol-proc calls only public methods ([1].each(&:puts) raises "private method 'puts' called for an instance of Integer"), consistent with public_send.

  • Hash#to_proc (Ruby 2.3) looks keys up: handy with map, select and filter_map, where an absent key gives nil.

  • A class that defines to_proc can be used after &, which is how value objects, specifications and "predicate objects" are plugged into select and map. to_proc should return a Proc (a lambda is best); anything else raises TypeError ("can’t convert Bad to Proc (Bad#to_proc gives Integer)").

  • Symbol#to_proc has been in the language since Ruby 1.8.7. Procs created from symbols are lambdas, which is why their arity is -2 and not 1.

Currying and composition

Proc#curry returns a curried proc that collects arguments until it has enough, then calls the original. The number needed is the arity (for a lambda: the required parameters), or the argument you pass: curry(n). A lambda with a fixed arity rejects a wrong n with ArgumentError; one with a splat or optional arguments accepts it. Method#curry works the same way.

Proc#>> and Proc#<< (Ruby 2.6, Method versions 2.7) compose callables: f >> g runs f first and then g on its result; f << g runs g first. The right-hand side can be any object with call (a Proc, a Method or your own class); anything else raises TypeError ("callable object is expected"). The result is a lambda when the callable that runs first is one.

# frozen_string_literal: true

add = ->(a, b) { a + b }
p add.curry[1][2], add.curry.call(1).call(2), add.curry.call(1, 2)
# => 3
# => 3
# => 3

add_three = add.curry[3]
p [1, 2].map(&add_three)
# => [4, 5]

variadic = ->(*numbers) { numbers.sum }
p variadic.curry(3)[1][2][3]
# => 6

begin
  add.curry(3)
rescue ArgumentError => e
  puts "ArgumentError: #{e.message}"
end
# prints: ArgumentError: wrong number of arguments (given 3, expected 2)

p proc { |a, b| [a, b] }.curry[1][2]
# => [1, 2]

# >> runs the receiver first, << runs the argument first.
strip = :strip.to_proc
squeeze = ->(text) { text.squeeze(" ") }
title_case = ->(text) { text.split.map(&:capitalize).join(" ") }

normalize = strip >> squeeze >> title_case
p normalize.call("  the   left hand  of darkness ")
# => "The Left Hand Of Darkness"
p (title_case << squeeze << strip).call("  the   left hand ")
# => "The Left Hand"

# Methods compose too, and any object that responds to call works as the other side.
p (method(:Integer) >> ->(n) { n * 2 }).call("21")
# => 42
class Doubler
  def call(number) = number * 2
end
p (add_three >> Doubler.new).call(1)
# => 8
p (proc { |x| x } >> proc { |x| x }).lambda?, (->(x) { x } >> proc { |x| x }).lambda?
# => false
# => true

begin
  add_three >> 5
rescue TypeError => e
  puts "TypeError: #{e.message}"
end
# prints: TypeError: callable object is expected

Currying is useful for partial application (add_three, passed to map above) and for building specialised validators or formatters from one general lambda. Composition reads best as a pipeline of small named lambdas (normalize = strip >> squeeze >> title_case), and steps.inject(:>>) folds a list of callables into one. Ruby does not reorder or optimise a chain, so use it for clarity rather than speed.

Callable objects as interfaces

call is the closest thing Ruby has to a function type: anything that responds to call can be used where a function is expected, which makes procs, lambdas, Method objects and your own classes interchangeable. Four idioms rest on it.

  • Duck-typed interfaces. A collaborator that needs one behaviour takes a callable instead of a class hierarchy. The Rack web interface is the famous case: an application is any object with call(env) returning [status, headers, body], and a middleware is a callable that wraps another callable. The example below builds that shape without any web library.

  • Chains and pipelines. A list of callables folded with inject(:>>) (above) or reduce over call.

  • Strategies and configuration. A hash of lambdas, or a lambda stored in a configuration object, replaces a strategy class when the behaviour is one function; see SOLID and Design Patterns for when a class is better.

  • Matchers. === on a lambda calls it, so lambdas work in case/when, in grep (array.grep(adult)) and as pinned values in pattern matching (^adult); see Pattern Matching.

# frozen_string_literal: true

require "shelf"

# The Rack contract in miniature: anything with call(env) returning [status, headers, body].
app = ->(env) { [200, { "content-type" => "text/plain" }, ["Hello #{env[:user]}"]] }

# A middleware is a callable that wraps another callable.
class Timing
  def initialize(app) = @app = app

  def call(env)
    status, headers, body = @app.call(env)
    [status, headers.merge("x-handled" => "yes"), body]
  end
end

stack = Timing.new(app)
p stack.call({ user: "ana" })
# => [200, {"content-type" => "text/plain", "x-handled" => "yes"}, ["Hello ana"]]

# A chain of callables is a fold.
steps = [:strip.to_proc, :downcase.to_proc, ->(text) { text.tr(" ", "-") }]
slug = steps.inject(:>>)
p slug.call("  Left Hand Of Darkness ")
# => "left-hand-of-darkness"

# Callables as strategies and configuration.
Shelf.configure { |config| config.loan_days = 21 }
late_fee = {
  flat: ->(days_late) { days_late.positive? ? 1.0 : 0.0 },
  daily: ->(days_late) { [days_late, 0].max * 0.25 }
}
p(late_fee.transform_values { |strategy| strategy.call(4) })
# => {flat: 1.0, daily: 1.0}
p Shelf.config.loan_days
# => 21

# === makes lambdas usable in case/when and in pattern matching.
adult = ->(age) { age >= 18 }
age = 30
case age
when adult then puts "adult"
end
person = { age: }
case person
in { age: ^adult } then puts "adult (pin)"
end
# prints: adult
# prints: adult (pin)

Design guidance:

  • Prefer a plain object with call over a lambda once the callable has state, several collaborators or its own tests. A small class is easier to name, document and mock than a lambda stored in a hash.

  • The pattern def self.call(...) = new(...).call (a class-level call that builds the object and runs it) makes a class usable as a callable and as a function-like "service object"; the forwarding syntax is on Calling Methods and Argument Forwarding.

  • Give a callable’s parameters real names, and document whether a lambda or a proc is expected. A proc argument’s lenient arity is a frequent source of silently wrong data; a lambda fails early.

  • Method objects (&method(:name)) let existing methods plug into the same slots without a wrapper lambda.

The shelf Catalog in practice

The shelf Catalog is a small, complete use of the block machinery: each yields when given a block and otherwise returns an enumerator, uses the anonymous & to forward the block to Array#each, and the Enumerable mixin turns that one method into map, select, each_with_index, min_by and the rest.

# frozen_string_literal: true

require "shelf"

catalog = Shelf::Catalog.new
{ "Dune" => "9780441172719", "Emma" => "9780306406157" }.each do |title, digits|
  catalog.add(Shelf::Book.new(isbn: Shelf::Isbn.new(digits:), title:, author: Shelf::Author.new(name: "Anon")))
end

# Catalog#each yields when given a block and returns an Enumerator otherwise,
# so every Enumerable method (and external iteration) works.
catalog.each { |book| puts book.title }
# prints: Dune
# prints: Emma
p catalog.each.class
# => Enumerator
p catalog.each.next.title
# => "Dune"
p catalog.map(&:title)
# => ["Dune", "Emma"]
p(catalog.each_with_index.map { |book, index| "#{index}: #{book.title}" })
# => ["0: Dune", "1: Emma"]

Extracting block bodies

When a block body grows past a line or two, or is reused, give it a name. Three tools do this, in the order to try:

  1. A method (private if internal) and &method(:name) or a plain call inside the block: the block becomes each { |loan| renew(loan) }.

  2. A lambda assigned to a local variable or constant, passed with &: loans.select(&overdue).

  3. A callable object when it needs state or tests.

# frozen_string_literal: true

require "shelf"

def book(title, digits)
  Shelf::Book.new(isbn: Shelf::Isbn.new(digits:), title:, author: Shelf::Author.new(name: "Anon"))
end

loans = [
  Shelf::Loan.new(book("Dune", "9780441172719"), state: :lent),
  Shelf::Loan.new(book("Emma", "9780306406157"), state: :overdue),
  Shelf::Loan.new(book("Hobbit", "9780261103252"))
]

# Name a non-trivial block body once, then reuse it where a block is expected.
overdue = ->(loan) { loan.state == :overdue }
title_of = ->(loan) { loan.book.title }

p loans.select(&overdue).map(&title_of)
# => ["Emma"]
p loans.reject(&overdue).map(&:state)
# => [:lent, :available]
p loans.partition(&overdue).map(&:size)
# => [1, 2]
puts loans.map(&:describe)
# prints: Dune is lent
# prints: Emma is overdue
# prints: Hobbit is on the shelf

Iterators (each, map, select, reject, flat_map, filter_map, take_while, inject) express a loop with the intent visible; for and while are for algorithms that need explicit index control. Counting loops have iterators as well (times, upto, downto, step). See Enumerable and Idioms, Style and Clean Code.

Pitfalls and diagnostics

Symptom Cause Fix

LocalJumpError: no block given (yield)

The method was called without a block

Guard with block_given?, or return enum_for(__method__)

LocalJumpError: unexpected return or break from proc-closure

A proc’s return or break ran after its method finished

Use a lambda, or next

ArgumentError: wrong number of arguments from each(&lambda)

A lambda used as a block receives the arguments of yield strictly

Use a proc/block, or give the lambda one parameter for the pair

puts list.map do ... end prints #<Enumerator ...>

do ... end binds to puts, not map

Braces, a local variable, or parentheses

NameError: undefined local variable or method 'y'

A variable first assigned inside a block is invisible outside it

Assign it before the block

Every lambda in a for loop returns the last value

for shares one variable across iterations

Use each/map so each pass has its own parameter

NoMethodError with &:first over each_with_index

The symbol-proc takes the first yielded value as receiver and the rest as arguments

Use an explicit block: { |(item, _), index| ... }

ArgumentError: tried to create Proc object without a block

Proc.new/proc with no block (Ruby 3.0+)

Pass &block explicitly

ArgumentError: the lambda method requires a literal block

lambda(&some_proc) (Ruby 3.3+)

Write lambda { ... } or pass the proc as is

SyntaxError: numbered parameters are not allowed when an ordinary parameter is defined

_1 or it mixed with |x|

Use one style per block

SyntaxError: Invalid yield

yield at class level or inside define_method

Take &block as a parameter

TypeError: no implicit conversion of Object into Proc

&obj where obj has no to_proc

Define to_proc, or pass a real Proc

TypeError: callable object is expected

>> or << given something without call

Pass a Proc, Method or object with call

Callback sees a changed variable

Closures capture variables, not values

Copy the value into a block parameter or a new method argument

Memory stays high after a method returns

A long-lived closure keeps its whole scope alive

Create it in a small method, or drop big locals first

Style and RuboCop

Default RuboCop settings flag these block-related patterns; the rules summarise the community style:

Cop Rule

Style/BlockDelimiters

Braces for single-line blocks, do ... end for multi-line (line_count_based)

Style/Lambda

-> for single-line lambdas, lambda for multi-line ones (line_count_dependent)

Style/Proc

proc { } instead of Proc.new { }

Style/LambdaCall

l.call(x) instead of l.(x)

Style/SymbolProc

map(&:upcase) instead of map { |s| s.upcase }

Style/CaseEquality

Avoid calling === directly

Lint/AmbiguousBlockAssociation

Parenthesise a call that takes another call’s block, such as p(list.map { ... })

Style/ItBlockParameter

Prefer it over _1 in single-line blocks, and avoid it in multi-line blocks (allow_single_line)

Style/NumberedParameters, Style/NumberedParametersLimit

Numbered parameters only in single-line blocks, at most one

Style/RedundantReturn

No return as the last expression of a method or lambda

Lint/UselessAssignment

A block-local variable that is assigned and never read

Style/For

Use each instead of for

The examples in this page that illustrate a form RuboCop rewrites (Proc.new, lambda { }, .(), ===, for, multiple numbered parameters) carry a short rubocop:disable comment so that the listings stay clean under the default configuration. Style/ItBlockParameter and Style/NumberedParameters are "pending" cops, which run with NewCops: enable as in the shelf configuration. The shelf gem’s own configuration is in the Style, Linting and Static Analysis page.

Version notes

Version Change

2.6

Proc#>> and Proc#<<; no warning for shadowed block variables

2.7

Numbered parameters _1 to _9; Proc.new without a block deprecated; Method#>> and Method#<<

3.0

Proc.new and proc without a block raise ArgumentError; Hash#each yields one pair, so a two-parameter lambda passed to it raises ArgumentError; Proc#== and eql? for procs created from the same block; _1 assignment is a syntax error

3.1

Anonymous block parameter & and its forwarding

3.2

A proc with one positional parameter and keywords no longer auto-splats an array

3.3

lambda(&proc) raises ArgumentError

3.4

The it block parameter; Prism parser by default

4.0

Binding#implicit_parameters, implicit_parameter_get, implicit_parameter_defined?; Binding#local_variables no longer lists numbered parameters; Proc#parameters shows [:opt] for anonymous optional parameters; Ractor.shareable_proc and Ractor.shareable_lambda

The exact release notes are in the What’s New and Migrating Older Code page.

Where older material is outdated

Most printed and online introductions to Ruby predate several of these changes. When following a book or a blog post that covers blocks, check these points:

Older text says Ruby 4.0

Numbered parameters (_1) are the shortest block syntax

The it parameter (3.4) is the idiomatic single-parameter shorthand; _1 remains for several parameters

Symbol#to_proc arrived in Ruby 1.9

It was backported to 1.8.7 and is now an everyday idiom; it is also a lambda

Proc.new or proc with no block captures the method’s block

ArgumentError since 3.0; pass &block

lambda(&proc) converts a proc

ArgumentError since 3.3

A lambda passed as a block gets the elements of a yielded array as separate arguments

Lambdas are strict: Hash#each(&->(k, v) { }) has raised ArgumentError since 3.0; use a block or a one-parameter lambda

Hash printed as {:name=>"Ico"}

{name: "Ico"} since 3.4; update any output you compare against

Method definitions always written def name(args) ... end

Endless methods (3.0), value omission in hashes and keywords (3.1) and anonymous &, *, ** forwarding (3.1 and 3.2) are common in current code

Examples with Struct.new or Data.define inheritance (class X < Struct.new(...))

Assign the result (X = Data.define(...) do ... end); see Struct, Data and Value Objects

References