Lambdas
A lambda is Ruby’s version of an anonymous function that behaves like a real method: it checks how many arguments you pass it, and a return inside one exits only the lambda itself, not the code around it. Lambdas are one of three closely related ways Ruby lets you package up a chunk of code as an object – blocks, procs, and lambdas – and understanding exactly how a lambda differs from a plain proc is one of the most important distinctions in idiomatic Ruby. This lesson covers how lambdas are created, how they’re called, how their strict argument checking and method-like return set them apart, and where to reach for a lambda instead of a proc or block.
Overview: How Lambdas Work
Ruby is a language where nearly everything is an object – integers, strings, even nil and false are instances of a class (NilClass, FalseClass), and you can call methods on all of them. A lambda takes this philosophy further: it turns a block of code into a first-class object you can store in a variable, pass to a method, and call whenever you like. Under the hood, a lambda is an instance of the Proc class, created with an internal flag set to lambda mode. You can check this flag yourself by calling lambda? on any Proc object – it returns true for a lambda and false for an ordinary proc.
That lambda-mode flag changes two important behaviors compared to a plain proc:
- Argument checking. A lambda checks the number of arguments it receives just like a normal method does. Call it with too few or too many arguments and it raises an
ArgumentError. A proc, by contrast, is lenient: missing arguments becomeniland extra arguments are silently discarded. - Return semantics. A
returninside a lambda exits only the lambda, handing control back to whatever called it – exactly like returning from a method. Areturninside a proc exits the enclosing method that the proc was defined in, which can produce surprising control flow if you aren’t expecting it.
Blocks, procs, and lambdas are all ways of capturing a closure: a chunk of code plus the local variables that were in scope where it was defined. A block is not an object at all; it only exists as the thing following do...end or { } when you call a method. A proc or a lambda, on the other hand, is a genuine object: it has a class, responds to methods, and can be passed around, stored, and called on its own schedule. This is what makes lambdas useful as callbacks, event handlers, and stored strategies.
Syntax
There are two equivalent ways to create a lambda, and three equivalent ways to call one:
->(params) { body } # "stabby lambda" literal - preferred modern style
lambda { |params| body } # lambda keyword with a block - equivalent meaning
my_lambda.call(args) # explicit call
my_lambda.(args) # call shorthand
my_lambda[args] # call via [] - reads like a collection lookup
| Piece | Meaning |
|---|---|
-> |
Begins a stabby lambda literal |
(params) |
Parameter list, checked strictly for arity |
{ body } |
The lambda’s body; its last expression is the return value |
.call / .() / [] |
Three interchangeable ways to invoke the lambda |
Examples
Example 1: Creating and calling lambdas
Ruby gives you two equivalent ways to write a lambda literal, and three equivalent ways to call one:
square = lambda { |x| x * x }
cube = ->(x) { x ** 3 }
puts square.call(5)
puts cube.call(3)
puts square.(4)
puts square[6]
Output:
25
27
16
36
Both square and cube are lambdas – one built with the lambda keyword and a block, the other with the stabby -> literal. They behave identically once created. The three call styles, .call(4), .(4), and [4], are all aliases for the same underlying Proc#call method; which one you use is a matter of style, and .call is the clearest for readers new to the code.
Example 2: Strict argument checking
Because a lambda behaves like a method, calling it with the wrong number of arguments raises an ArgumentError immediately – this is one of the biggest practical differences from a proc:
add = ->(a, b) { a + b }
begin
add.call(1)
rescue ArgumentError => e
puts "Error: #{e.message}"
end
puts add.call(2, 3)
Output:
Error: wrong number of arguments (given 1, expected 2)
5
The first call passes only one argument to a lambda that expects two, so Ruby raises ArgumentError before the lambda’s body even runs, exactly as it would for a regular method call with a missing argument. The rescue catches it and prints the message. The second call passes both arguments correctly and returns 5. If add were an ordinary Proc instead of a lambda, add.call(1) would not raise anything – b would simply be nil, which can silently produce wrong results.
Example 3: How return behaves differently
This is the other core distinction, and it matters a lot once lambdas and procs are used inside methods:
def test_lambda
l = -> { return 10 }
l.call
20
end
def test_proc
p = Proc.new { return 10 }
p.call
20
end
puts test_lambda
puts test_proc
Output:
20
10
Inside test_lambda, calling l.call runs return 10, but because l is a lambda, that return only exits the lambda itself. Execution continues to the next line, and the method returns 20, the value of its last expression. Inside test_proc, the Proc.new block also runs return 10, but because it is a plain proc, return exits the enclosing method, test_proc, immediately – the line 20 is never reached, and the method returns 10.
How It Works Step by Step
Walking through square = ->(x) { x * x }; square.call(5):
- The
->(x) { x * x }literal creates a newProcobject with its internal lambda flag set totrue. Nothing runs yet – the body is just stored, along with a reference to the surrounding scope, forming a closure. - Assigning it to
squarestores a reference to that object, the same as assigning any other object to a variable. - Calling
square.call(5)invokesProc#callon the object. Because the lambda flag is set, Ruby first checks arity: the lambda expects exactly one argument, and one was given, so the check passes. - Ruby binds the parameter
xto5inside the lambda’s own local scope. - The body
x * xexecutes and evaluates to25. - Since there is no explicit
return, the value of the last evaluated expression,25, becomes the lambda’s return value, exactly like a method whose body ends without an explicitreturn.
The key mechanical difference from a proc happens in the middle steps: arity checking is strict, and any return encountered inside the body targets the lambda’s own call frame rather than reaching further out to whatever method the lambda happens to be defined inside.
Common Mistakes
Mistake 1: Assuming a proc validates arguments like a lambda
A proc silently accepts the wrong number of arguments, which can hide a bug instead of raising an error:
greet = Proc.new { |name, greeting| "#{greeting}, #{name}!" }
puts greet.call("Ada")
Output:
, Ada!
The caller forgot to pass greeting, but the proc doesn’t complain – it just binds greeting to nil, and the string interpolates it as an empty string, producing garbled output instead of an error. If argument correctness matters, use a lambda instead, so a missing argument fails loudly:
greet = ->(name, greeting) { "#{greeting}, #{name}!" }
puts greet.call("Ada", "Hello")
Output:
Hello, Ada!
Mistake 2: Using return inside a proc created outside any method
Because a proc’s return tries to exit its enclosing method, calling one where there is no enclosing method raises a LocalJumpError:
bad_proc = Proc.new { return 5 }
bad_proc.call
puts "after"
Running this raises LocalJumpError: unexpected return and the line printing “after” is never reached. A lambda does not have this problem, because its return only ever exits itself:
good_lambda = -> { 5 }
puts good_lambda.call
puts "after"
Output:
5
after
Since a lambda’s last expression is already its return value, an explicit return is optional here – either form works safely inside a lambda.
Best Practices
- Reach for a lambda when you want method-like behavior – strict argument checking and a
returnthat only affects the lambda itself – which is exactly what you want for callbacks, validators, and stored strategies. - Prefer the
->(params) { body }stabby lambda literal over thelambda { |params| body }keyword form for short lambdas; it’s the style most current Ruby style guides and linters recommend. - Use
.callrather than.()or[]in code meant to be read by newcomers – it’s the most explicit and searchable. - Check
lambda?on aProcobject if you ever need to confirm at runtime whether you’re holding a lambda or a plain proc. - When an API accepts a callable argument, document whether it expects lambda or proc semantics, since the arity and
returnbehavior differ – mixing them up is a common source of confusing bugs. - Use
&:method_name(Symbol#to_proc) instead of a full lambda when you just need to call one method on each element, for examplenames.map(&:upcase)instead ofnames.map { |n| n.upcase }.
Practice Exercises
- Write a lambda called
largerthat takes two numbers and returns the greater of the two (hint: useArray#maxor a conditional). Call it with(4, 9)and(15, 3)and print both results. - Write a method
process(value:, validator:)that calls thevalidatorlambda withvalueand prints"valid"if it returns a truthy value or"invalid"otherwise. Pass in a lambda like->(n) { n.positive? }and test it with a positive and a negative number. - Without running it, predict what happens: a method defines a proc containing
return "early"inside a conditional, calls the proc, and then has more code after the call. Does the method ever reach that later code? Check your reasoning against Example 3 above.
Summary
- A lambda is a
Procobject created with->(params) { body }orlambda { |params| body }; checklambda?to tell one apart from a plain proc. - Lambdas enforce strict arity, raising
ArgumentErrorfor the wrong number of arguments, just like a method call. - A
returninside a lambda exits only the lambda; areturninside a proc exits the enclosing method, and raisesLocalJumpErrorif there is no enclosing method. - Lambdas can be called with
.call(args),.(args), or[args]– all equivalent. - Prefer lambdas for callbacks and stored logic where predictable, method-like behavior matters; reach for a plain block when you don’t need an object at all.
