
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:
my_lambda = -> { "hi" }
my_proc = proc { "hi" }
my_lambda.class # => Proc
my_lambda.lambda? # => true
my_proc.lambda? # => falseOne 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:
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:
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:
square = ->(val) { val**2 }
square.call(3) # => 9
square.(3) # => 9
square[3] # => 9
square === 3 # => 9The 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:
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:
square = -> { _1 * _1 }
square.call(4) # => 16
double = -> { it * 2 }
double.call(21) # => 42You can also pass a lambda where a method expects a block. Consider squaring a set of numbers:
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:
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:
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:
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:
def call_proc
p = proc { return 10 }
p.call
"this line never runs"
end
call_proc # => 10If the enclosing method has already returned, there is nothing left to exit, and the proc raises:
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:
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:
result = [1, 2, 3, 4].each do |n|
break n if n > 2
end
result # => 3A 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 &:
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:
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
| Behavior | Block | Proc | Lambda |
|---|---|---|---|
| Object? | No, syntax only | Yes, a Proc instance | Yes, a Proc instance; lambda? returns true |
| Arity checking | Loose: extra arguments dropped, missing ones nil | Loose, same as blocks | Strict: raises ArgumentError |
return | Exits the enclosing method | Exits the enclosing method; LocalJumpError if it already returned | Returns from the lambda only |
next | Ends the current yield with a value | Ends the current call with a value | Ends the current call with a value |
break | Exits the yielding method with a value | Raises LocalJumpError (break from proc-closure) | Ends the lambda call, like return |
| Conversion | &block parameter captures it as a Proc | Pass with &; Symbol#to_proc creates one | Pass 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:
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) # => 95Each 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:
# 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:
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:
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] # => 6Because 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:
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:
def create_counter(start)
lambda { start += 1 }
end
counter = create_counter(0)
puts counter.call # => 1
puts counter.call # => 2The 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:
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) } }
endOne run on our test machine produced:
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?
- Subscribe to our Ruby Magic newsletter and never miss an article again.
- Start monitoring your Ruby app with AppSignal.
- Share this article on social media

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 BroemmerBecome our next author!
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!


