Procs vs Lambdas
Ruby gives you three flavors of “chunk of code you can pass around”: blocks, procs, and lambdas. Blocks aren’t objects at all, but procs and lambdas both are — and they’re actually the same class, Proc, under the hood. That makes it easy to assume they behave identically, but they don’t. Procs and lambdas disagree on two important things: how strict they are about the number of arguments you pass, and what happens when you call return inside them. Get this distinction wrong and you’ll write code that either silently swallows bugs or crashes a method you didn’t expect to exit.
Overview: How Procs and Lambdas Work
In Ruby, a block (the code between do...end or {...} attached to a method call) is not an object on its own — it’s just syntax that Ruby hands to the method via yield. The moment you capture that code as a value — with Proc.new, the proc method, lambda, or the -> literal — Ruby wraps it in an instance of the Proc class. Because everything in Ruby is an object (even integers, even nil), a captured block becomes a first-class value you can store in a variable, pass to a method, return from a method, and call later with .call.
So where do procs and lambdas differ if they’re both instances of the same Proc class? Internally, every Proc object carries a hidden boolean flag, checked with the lambda? method, that tells Ruby whether it should behave “strictly” (like a lambda) or “leniently” (like a plain proc). That single flag controls two behaviors:
- Argument checking. A lambda checks its argument count exactly like a normal method call would, raising
ArgumentErroron a mismatch. A proc is forgiving: missing arguments becomenil, and extra arguments are simply discarded. returnsemantics. Callingreturninside a lambda exits only the lambda, handing control back to whatever called it — just like returning from an ordinary method. Callingreturninside a proc exits the enclosing method the proc was defined in, skipping everything after the point where the proc was called. If a proc containingreturnis called outside of any enclosing method, Ruby raises aLocalJumpErrorbecause there’s nothing to return from.
You’ll also see :some_symbol.to_proc and method(:some_method).to_proc in real code — both produce plain (non-lambda) Proc objects, which is part of why &:upcase-style shorthand behaves leniently on arity if you ever call it directly.
Syntax
There are two common ways to build each kind, and they’re interchangeable within their category:
ps1 = Proc.new { |x| x * 2 }
ps2 = proc { |x| x * 2 }
lm1 = lambda { |x| x * 2 }
lm2 = ->(x) { x * 2 }
| Form | Creates | Notes |
|---|---|---|
Proc.new { ... } |
Proc (lenient) | Must be given an explicit block as of Ruby 3.0 |
proc { ... } |
Proc (lenient) | Kernel method, most common way to write a plain proc |
lambda { ... } |
Proc (strict / lambda? true) |
Kernel method |
->(args) { ... } |
Proc (strict / lambda? true) |
The “stabby lambda” literal; preferred in modern style guides |
Both kinds are called the same way, using any of three interchangeable syntaxes: my_callable.call(args), my_callable.(args), or my_callable[args].
Examples
Example 1: Creating, calling, and telling them apart
my_proc = Proc.new { |x| x * 2 }
my_lambda = lambda { |x| x * 2 }
my_lambda2 = ->(x) { x * 2 }
puts my_proc.call(5)
puts my_proc.(5)
puts my_proc[5]
puts my_lambda.call(5)
puts my_lambda2.call(5)
puts my_proc.class
puts my_lambda.class
puts my_proc.lambda?
puts my_lambda.lambda?
Output:
10
10
10
10
10
Proc
Proc
false
true
All three call syntaxes (.call, .(), and []) are pure syntax sugar for the same thing, and work identically on both procs and lambdas. Notice that my_lambda.class prints Proc, not some separate “Lambda” class — a lambda is a Proc whose lambda? flag happens to be true.
Example 2: Argument strictness
lenient_proc = Proc.new { |a, b| "a=#{a.inspect}, b=#{b.inspect}" }
strict_lambda = lambda { |a, b| "a=#{a.inspect}, b=#{b.inspect}" }
puts lenient_proc.call(1)
puts lenient_proc.call(1, 2, 3)
puts strict_lambda.call(1, 2)
begin
strict_lambda.call(1)
rescue ArgumentError => e
puts "ArgumentError: #{e.message}"
end
Output:
a=1, b=nil
a=1, b=2
a=1, b=2
ArgumentError: wrong number of arguments (given 1, expected 2)
The proc happily accepts one argument (filling b with nil) or three (silently dropping the third). The lambda enforces exactly two, raising the same kind of ArgumentError you’d get from calling an ordinary method with the wrong number of arguments.
Example 3: Return semantics
def proc_return_demo
puts "before proc call"
my_proc = Proc.new { return "returned from proc" }
my_proc.call
puts "this line never runs"
end
def lambda_return_demo
puts "before lambda call"
my_lambda = lambda { return "returned from lambda" }
result = my_lambda.call
puts "lambda gave back: #{result}"
"returned from method"
end
puts proc_return_demo
puts lambda_return_demo
Output:
before proc call
returned from proc
before lambda call
lambda gave back: returned from lambda
returned from method
Inside proc_return_demo, calling the proc triggers its return, which immediately exits the whole method — "this line never runs" is skipped entirely, and the method’s return value becomes whatever the proc returned. Inside lambda_return_demo, the lambda’s return only exits the lambda itself; execution resumes right after my_lambda.call, the next puts runs normally, and the method finishes on its own final expression.
How It Works Step by Step
Walking through Example 3’s call to proc_return_demo:
- Ruby prints
"before proc call". - A
Procobject is created and assigned tomy_proc. Nothing inside it runs yet — creating a proc just packages up the code. my_proc.callexecutes the proc’s body. It hitsreturn "returned from proc".- Because this proc is non-strict (not a lambda), its
returnis scoped to the method it was defined in —proc_return_demo— not just the proc. Ruby immediately unwinds out ofproc_return_demowith that value. - The line
puts "this line never runs"is never reached, because the method already exited. - Back at the top level,
puts proc_return_demoreceives"returned from proc"as the method’s return value and prints it.
The lambda version differs at exactly one step: its return only unwinds the lambda’s own call frame, so control returns to the line right after my_lambda.call, and lambda_return_demo keeps running until it reaches its own final expression.
Common Mistakes
Mistake 1: Expecting a lambda’s return to break out of an enclosing loop
A very common pattern is using a proc as an “early exit” search callback. People often reach for lambda out of habit, but a lambda’s return won’t propagate outward the way a proc’s does:
def find_first_negative_broken(numbers)
checker = lambda { |n| return n if n.negative? }
numbers.each(&checker)
"no negative found"
end
puts find_first_negative_broken([3, 5, -2, 8]).inspect
Output:
"no negative found"
Even though -2 is in the array, the lambda’s return only exits the lambda call inside each, so iteration just keeps going. The method never learns that a negative number was found. Swap in a proc, whose return is scoped to the enclosing method, and it works as intended:
def find_first_negative_fixed(numbers)
checker = Proc.new { |n| return n if n.negative? }
numbers.each(&checker)
"no negative found"
end
puts find_first_negative_fixed([3, 5, -2, 8]).inspect
Output:
-2
Here the proc’s return unwinds all the way out of find_first_negative_fixed as soon as it finds -2, short-circuiting the rest of the iteration.
Mistake 2: Trusting a proc’s lenient arity for something that must be exact
Because a proc never complains about a missing argument, a typo in a call site can produce silently wrong output instead of a helpful error:
build_greeting = Proc.new { |name, greeting| "#{greeting}, #{name}!" }
puts build_greeting.call("Ada")
Output:
, Ada!
The caller forgot the greeting argument. The proc doesn’t raise anything — it just fills greeting with nil, which interpolates as an empty string, producing a garbled greeting instead of a clear error. A lambda catches the same mistake immediately:
build_greeting = lambda { |name, greeting| "#{greeting}, #{name}!" }
begin
puts build_greeting.call("Ada")
rescue ArgumentError => e
puts "Caught bug immediately: #{e.message}"
end
Output:
Caught bug immediately: wrong number of arguments (given 1, expected 2)
This is exactly why lambdas are the safer default for anything that behaves like a small function: mistakes surface at the call site instead of drifting downstream as mysteriously wrong data.
Best Practices
- Default to lambdas (preferably the
->(args) { ... }literal) for small, function-like pieces of logic — validation, transformations, calculations — where you want strict argument checking and predictablereturnbehavior. - Reach for a plain
proc(or just a block) when you specifically want method-like early-exit control flow, such as a search callback that should unwind the enclosing method. - Don’t mix up the two mid-codebase for the same kind of callback — pick one convention for a given API so callers know what arity and return behavior to expect.
- Remember that as of Ruby 3.0,
Proc.newrequires its own explicit block; it can no longer implicitly grab a block passed to the surrounding method. - Check
lambda?when debugging unexpected argument or return behavior in code you didn’t write — it tells you immediately which rules a givenProcobject follows. - Prefer
->(x) { x * 2 }overlambda { |x| x * 2 }for short, self-contained lambdas; it’s the idiom modern Ruby style guides recommend.
Practice Exercises
- Write a lambda called
cubeusing the->literal that returns its argument raised to the third power, and printcube.call(3). - Write both a proc and a lambda that each take three keyword-style positional arguments. Call each with only two arguments and predict, before running, what each one does — then verify your prediction.
- Write a method
first_even(numbers)that uses aProc.newwith an earlyreturninsidenumbers.eachto return the first even number in the array, ornilif there isn’t one. Test it against[1, 3, 4, 7]and[1, 3, 5].
Summary
- Procs and lambdas are both instances of the same
Procclass; a hidden flag, readable vialambda?, decides which rules they follow. - Lambdas check argument count strictly, raising
ArgumentErroron a mismatch; procs are lenient, padding missing arguments withniland discarding extras. returninside a lambda exits only the lambda;returninside a proc exits the method the proc was defined in.- Use lambdas for small, function-like logic where you want strict checking and predictable returns; use procs when you want method-like early-exit control flow.
- As of Ruby 3.0,
Proc.newalways needs its own explicit block — it can’t implicitly borrow one from the enclosing method.
