Ruby

Ruby Lambdas: Syntax, Lambda vs. Proc, and return/next Semantics

Ruby Lambdas: Syntax, Lambda vs. Proc, and return/next Semantics

In Ruby, a block is an anonymous chunk of code passed to a method; a proc is a block wrapped in an object; a lambda is a strict proc: it raises ArgumentError on wrong arity, and return and next both return control from the lambda itself — a proc’s return exits the enclosing method instead.

That is the short version. This guide covers the rest: lambda syntax on current Ruby, the exact return, next, and break rules, practical use cases, and what lambdas cost at runtime. Every example here runs on Ruby 3.4.

What Is a Lambda Function?

A lambda is an expression whose value is a function. Because that function has no name, lambdas are also called anonymous functions. You can store a lambda in a variable, pass it to a method, and call it later, the same way you handle a string or an integer.

Everything in Ruby is an object, and lambdas are no exception. A lambda is an instance of the Proc class; Ruby has no separate lambda class. What sets a lambda apart is stricter behavior: it validates its argument count and treats return the way a method does. Proc#lambda? tells you which flavor you hold:

Ruby
my_lambda = -> { "hi" }
my_proc = proc { "hi" }
 
my_lambda.class # => Proc
my_lambda.lambda? # => true
my_proc.lambda? # => false

One naming clarification: AWS Lambda, the cloud service, shares nothing with Ruby’s lambda beyond the name.

How to Define and Invoke a Lambda Function in Ruby

The canonical form uses the lambda method with a literal block. More commonly, you will see the -> literal:

Ruby
a_lambda = lambda { puts "Hello world!" }
 
# The more common arrow form:
a_lambda = -> { puts "Hello world!" }

Both forms accept do ... end for multi-line bodies. The arrow syntax takes a do ... end block too, with parameters in parentheses after the arrow:

Ruby
a_lambda = lambda do
  puts "Hello world!"
end
 
greet = ->(name) do
  puts "Hello, #{name}!"
end
 
greet.call("Ruby")
# Hello, Ruby!

Inspecting either form prints a Proc with a (lambda) marker, such as #<Proc:0x00007a60372e0380 example.rb:1 (lambda)>. Referencing greet on its own does nothing; that reads the variable without invoking the function. A lambda only runs when you invoke it, and Ruby offers four equivalent ways to do that:

Ruby
square = ->(val) { val**2 }
 
square.call(3) # => 9
square.(3)     # => 9
square[3]      # => 9
square === 3   # => 9

The last form looks cryptic until you see why it exists: Proc#=== lets a lambda act as a matcher in a case/when expression. A when clause holding a lambda calls it with the tested value and matches on a truthy result.

Unlike a proc, a lambda checks its argument count and raises when it is wrong:

Ruby
add = ->(a, b) { a + b }
add.call(1)
# => wrong number of arguments (given 1, expected 2) (ArgumentError)
 
loose = proc { |a, b| [a, b] }
loose.call(1, 2, 3) # => [1, 2]

For short lambdas, you can skip named parameters. Numbered parameters (_1, _2) have worked since Ruby 2.7, and Ruby 3.4 adds the it implicit parameter for the single-argument case:

Ruby
square = -> { _1 * _1 }
square.call(4) # => 16
 
double = -> { it * 2 }
double.call(21) # => 42

You can also pass a lambda where a method expects a block. Consider squaring a set of numbers:

Ruby
numbers = [1, 2, 3, 4, 5]
square_lambda = lambda { |n| n**2 }
numbers.map(&square_lambda) # => [1, 4, 9, 16, 25]

The ampersand converts the lambda into map’s block argument. Omit it, and Ruby treats square_lambda as a positional argument that map does not want, so numbers.map(square_lambda) raises wrong number of arguments (given 1, expected 0) (ArgumentError).

The conversion works in the other direction too, with one modern caveat. Since Ruby 3.3, the lambda method accepts only a literal block; passing an existing proc raises:

Ruby
my_proc = proc { |n| n * 2 }
lambda(&my_proc)
# => the lambda method requires a literal block (ArgumentError)

Ruby has no built-in way to convert a proc into a lambda, so decide which semantics you want when you create the object.

How return, next, and break Work in Ruby Lambdas

Argument checking is the visible difference between lambdas and procs. The control-flow keywords are the difference that bites in production. Here is what each one does, verified on Ruby 3.4.10.

What next Does in a Lambda

Inside a lambda, next ends the current call, and its argument becomes the lambda’s return value. Execution continues at the caller:

Ruby
classify = ->(n) do
  next "even" if n.even?
  "odd"
end
 
classify.call(2) # => "even"
classify.call(3) # => "odd"

next behaves identically in procs and blocks: it ends the current invocation with a value and never touches the enclosing method. That makes next the safe early-exit keyword in all three constructs.

What return Does in a Lambda vs. a Proc

return inside a lambda returns from the lambda itself, exactly like return in a method. The enclosing method keeps running:

Ruby
def call_lambda
  l = -> { return 10 }
  value = l.call
  "lambda returned #{value}"
end
 
call_lambda # => "lambda returned 10"

A proc’s return exits the enclosing method instead:

Ruby
def call_proc
  p = proc { return 10 }
  p.call
  "this line never runs"
end
 
call_proc # => 10

If the enclosing method has already returned, there is nothing left to exit, and the proc raises:

Ruby
def make_proc
  proc { return 10 }
end
 
make_proc.call
# => unexpected return (LocalJumpError)

At the top level of a script, a proc’s return follows top-level return semantics: calling it ends the whole script, with exit status 0. In a lambda, return inside a call, no matter where the lambda was defined, ends that call and nothing else.

What break Does in a Lambda vs. a Proc

In a lambda, break behaves like return: it ends the lambda call, and its argument becomes the return value:

Ruby
def call_lambda_break
  l = -> { break 10 }
  "lambda break value: #{l.call}"
end
 
call_lambda_break # => "lambda break value: 10"

In a literal block, break exits the method that yielded to the block, and its argument becomes that method’s return value:

Ruby
result = [1, 2, 3, 4].each do |n|
  break n if n > 2
end
 
result # => 3

A proc has no such method to break out of. Calling a proc that hits break raises, whether you call it directly or pass it to an iterator with &:

Ruby
p = proc { break 10 }
p.call
# => break from proc-closure (LocalJumpError)
 
p = proc { |n| break n if n > 2 }
[1, 2, 3, 4].each(&p)
# => break from proc-closure (LocalJumpError)

A lambda passed as a block keeps its own semantics. Its break ends that one lambda call, so iteration continues to the end:

Ruby
l = ->(n) { break n if n > 2 }
[1, 2, 3, 4].each(&l) # => [1, 2, 3, 4] (iteration completes)

Lambda vs. Proc vs. Block at a Glance

BehaviorBlockProcLambda
Object?No, syntax onlyYes, a Proc instanceYes, a Proc instance; lambda? returns true
Arity checkingLoose: extra arguments dropped, missing ones nilLoose, same as blocksStrict: raises ArgumentError
returnExits the enclosing methodExits the enclosing method; LocalJumpError if it already returnedReturns from the lambda only
nextEnds the current yield with a valueEnds the current call with a valueEnds the current call with a value
breakExits the yielding method with a valueRaises LocalJumpError (break from proc-closure)Ends the lambda call, like return
Conversion&block parameter captures it as a ProcPass with &; Symbol#to_proc creates onePass with &; lambda(&a_proc) raises on Ruby 3.3+

Lambda Use Cases in Ruby

First Class Functions

You can pass lambdas to other functions as values, a concept known as first-class functions. That lets the caller decide behavior, not only data:

Ruby
def apply_discount(order_total, discount)
  discount.call(order_total)
end
 
ten_percent_off = ->(total) { total * 0.9 }
flat_five_off = ->(total) { total - 5 }
 
apply_discount(100, ten_percent_off) # => 90.0
apply_discount(100, flat_five_off)   # => 95

Each discount rule is one line, and apply_discount never needs to change when you add another.

Callbacks

Lambdas also work well as callbacks: logic an object invokes at set points in its lifecycle. A real-world example comes from MongoDB’s Mongoid framework, where a field :default can be a lambda. The static version evaluates once, when the class loads. The lambda version runs every time a document is created, which is the timestamp you want:

Ruby
# Static: evaluated once, at class load time
field :last_modified, type: Time, default: Time.now
 
# Deferred: the lambda runs on each document creation
field :last_modified, type: Time, default: -> { Time.now }

The Visitor Pattern

The visitor pattern dispatches behavior based on an element’s type. A hash of lambdas keyed by class replaces a pile of near-identical methods:

Ruby
class PrintVisitor
  HANDLERS = {
    Integer => ->(node) { puts "Integer: #{node}" },
    String => ->(node) { puts "String: #{node.inspect}" }
  }
 
  def visit(node)
    HANDLERS.fetch(node.class).call(node)
  end
end
 
visitor = PrintVisitor.new
visitor.visit(42)      # Integer: 42
visitor.visit("hello") # String: "hello"

For two fixed types this may be over-engineered. It pays off when handlers are not known ahead of time, since callers can register new lambdas at runtime.

Functional Programming Paradigms

Lambdas bring functional programming techniques to Ruby: small, reusable functions combined into larger ones. Proc#>> and Proc#<< compose lambdas, and Proc#curry partially applies them:

Ruby
double = ->(n) { n * 2 }
increment = ->(n) { n + 1 }
 
(double >> increment).call(5) # => 11 (double first, then increment)
(double << increment).call(5) # => 12 (increment first, then double)
 
add = ->(a, b, c) { a + b + c }
add.curry[1][2][3] # => 6

Because lambdas check arity, composed pipelines fail loudly at the broken step instead of passing nil along.

Metaprogramming

Lambdas can carry method bodies around before they become methods. define_method accepts any proc object, including a lambda:

Ruby
class Greeter
  def self.create_method(name, body)
    define_method(name, &body)
  end
end
 
Greeter.create_method(:hello, -> { "Hello, world!" })
Greeter.new.hello # => "Hello, world!"

Lambdas as Closures

A lambda captures the variables in scope where it was defined. The function plus its captured environment is called a closure. Each call sees, and can change, the same captured variables:

Ruby
def create_counter(start)
  lambda { start += 1 }
end
 
counter = create_counter(0)
puts counter.call # => 1
puts counter.call # => 2

The start argument outlives create_counter because the lambda holds a binding to it.

The Impact of Lambdas on Performance

A lambda call goes through a Proc object rather than a direct method dispatch, so it costs slightly more. Here is a measured comparison on Ruby 3.4.10, using the Benchmark module from the standard library:

Ruby
require "benchmark"
 
ITERATIONS = 10_000_000
 
def square_method(n)
  n * n
end
 
def yield_square(n)
  yield n
end
 
square_lambda = ->(n) { n * n }
square_proc = proc { |n| n * n }
 
Benchmark.bm(15) do |x|
  x.report("method call:") { ITERATIONS.times { square_method(3) } }
  x.report("block yield:") { ITERATIONS.times { yield_square(3) { |n| n * n } } }
  x.report("lambda.call:") { ITERATIONS.times { square_lambda.call(3) } }
  x.report("proc.call:") { ITERATIONS.times { square_proc.call(3) } }
end

One run on our test machine produced:

Shell
                      user     system      total        real
method call:      0.454117   0.000000   0.454117 (  0.454375)
block yield:      0.638132   0.000000   0.638132 (  0.638182)
lambda.call:      0.527854   0.000000   0.527854 (  0.527972)
proc.call:        0.543969   0.000000   0.543969 (  0.544230)

Across 10 million iterations, lambda.call took about 0.53 seconds against 0.45 for a plain method call. That is under 10 nanoseconds of overhead per call. Micro-benchmarks like this vary between runs and machines; lambda.call ranged from 0.53 to 0.70 seconds across our runs. In any code path that touches a database or the network, this difference disappears into the noise.

If lambda-heavy code paths matter to your app’s performance, you can wrap them in a custom instrumentation span to measure their real production cost.

Wrapping Up

We pinned down the semantics that separate lambdas from procs and blocks: strict arity, return and break staying inside the lambda, and next as the universal early exit.

Lambdas can be convenient one-time functions, callback implementations, and building blocks for a functional style. And when you want visibility into how your lambda-based code behaves for real users, AppSignal for Ruby monitors your app’s performance in production.

Happy coding!

P.S. If you’d like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

Frequently asked questions

What does next return from a Ruby lambda?
Inside a lambda, next ends the current call and its argument becomes the lambda’s return value. The enclosing method keeps running. Procs behave the same way, which makes next the safe early-exit keyword in both.
What is the difference between return in a lambda and in a proc?
A lambda’s return exits the lambda itself, and the enclosing method continues. A proc’s return exits the enclosing method. If that method has already returned, calling the proc raises LocalJumpError with the message unexpected return.
Can you write a Ruby lambda with do and end syntax?
Yes. Both lambda do end and the arrow form -> do end are valid on every current Ruby version, including 3.4. Developers usually reserve do and end for multi-line lambdas and braces for one-liners.

Published , Updated

Wondering what you can do next?

  • Share this article on social media
Darren Broemmer

Darren Broemmer

Darren enjoys inspiring through the written word and making complex things easy to understand. His interests include science and physics, and enough math to make sense of them both. He creates high-quality content and technology solutions, and tweets occasionally about it.

All articles by Darren Broemmer

Become our next author!

Find out more
$appsignal install

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal