Proc vs Lambda in Ruby — Two Callable Objects, Different Rules
Ruby has two closely related callable objects — Proc and lambda — and the difference between them trips up developers more than almost any other Ruby concept. They look similar, they’re both instances of Proc, and they both wrap executable code. But they handle argument checking and return differently in ways that produce hard-to-debug behavior if you don’t know the rules.
What They Are
Both Proc and lambda are objects that wrap a block of code and can be called later. Both are instances of the Proc class. The lambda? method is the only way to tell them apart at runtime.
Example:
# Creating a Proc
square_proc = Proc.new { |n| n * n }
square_proc.call(5) # => 25
square_proc.class # => Proc
square_proc.lambda? # => false
# Creating a lambda
square_lambda = lambda { |n| n * n }
square_lambda.call(5) # => 25
square_lambda.class # => Proc
square_lambda.lambda? # => true
# Lambda literal syntax (Ruby 1.9+)
square_arrow = ->(n) { n * n }
square_arrow.lambda? # => true
# Calling syntax options
square_lambda.call(5) # => 25
square_lambda.(5) # => 25
square_lambda[5] # => 25
Difference 1: Argument Checking
Lambdas enforce arity. Procs ignore extra arguments and substitute nil for missing ones.
Example:
strict = lambda { |a, b| "#{a}, #{b}" }
lenient = Proc.new { |a, b| "#{a}, #{b}" }
strict.call(1, 2) # => "1, 2"
strict.call(1) # => ArgumentError: wrong number of arguments (given 1, expected 2)
strict.call(1, 2, 3) # => ArgumentError: wrong number of arguments (given 3, expected 2)
lenient.call(1, 2) # => "1, 2"
lenient.call(1) # => "1, " (b is nil)
lenient.call(1, 2, 3) # => "1, 2" (3 is silently ignored)
Example:
# Procs treat extra args in splat-like fashion
flexible = Proc.new { |a, *b, c| [a, b, c] }
flexible.call(1, 2, 3, 4) # => [1, [2, 3], 4]
flexible.call(1, 2) # => [1, [], 2]
flexible.call(1) # => [1, [], nil]
The lambda behavior is what you’d expect from a method. The Proc behavior matches how blocks handle arguments — Ruby blocks always accept any number of arguments without error.
Difference 2: Return Behavior
This is the more consequential difference. A return inside a lambda returns from the lambda itself. A return inside a Proc returns from the enclosing method.
Example:
def lambda_return
my_lambda = lambda { return 10 }
result = my_lambda.call
puts "Lambda returned: #{result}"
"method continues"
end
lambda_return
# Lambda returned: 10
# => "method continues" ← method keeps running after lambda returns
Example:
def proc_return
my_proc = Proc.new { return 10 }
my_proc.call
puts "This line never runs"
"neither does this"
end
proc_return
# => 10 ← proc's return exits the entire method
Example:
# The danger: Proc return outside a method raises LocalJumpError
orphan_proc = Proc.new { return 10 }
# Calling it at top level — no enclosing method to return from
orphan_proc.call # => LocalJumpError: unexpected return
This is why Procs shouldn’t be stored and called in contexts different from where they were created. If the method that created the Proc has already returned, calling the Proc with return inside raises LocalJumpError.
next and break in Procs vs Lambdas
The return-semantic difference extends to next and break.
Example:
# next in a Proc — exits the Proc, not the method
def next_in_proc
my_proc = Proc.new { next 10; puts "never" }
result = my_proc.call
puts "after proc: #{result}" # nil — next doesn't return a value from the proc
"method finishes"
end
next_in_proc
# after proc:
# => "method finishes"
# next in a lambda — same as return (exits lambda)
next_in_lambda = lambda { next 10; puts "never" }
next_in_lambda.call # => 10
Example:
# break in a Proc called from a method — exits the method with the break value
def break_in_proc
[1, 2, 3].each do |i|
break i * 10 if i == 2 # break exits each, returns value to the method
end
end
break_in_proc # => 20
# break in a lambda — same as return (exits lambda)
break_lambda = lambda { break 99 }
break_lambda.call # => 99
When to Use Each
Use lambda when:
- You need argument checking (calling with wrong arity should raise an error)
- The callable will be stored and called in a different context from where it was defined
- You want predictable return behavior (return exits the lambda, not the caller)
- Writing method-like callbacks, validators, or transformations
Use Proc when:
- You’re working with blocks and need to capture one as an object
- The callable is tightly coupled to its creation context (closures over local variables)
- You’re using
&to pass procs as blocks to iterators - You want block-like argument flexibility (extra args silently dropped)
Example:
# Lambda as a transformer — clean, predictable
multiply_by = ->(factor) { ->(n) { n * factor } }
double = multiply_by.(2)
triple = multiply_by.(3)
double.call(5) # => 10
triple.call(5) # => 15
# Proc with & — converting to/from blocks
def apply_all(array, &block)
array.map(&block)
end
apply_all([1, 2, 3]) { |n| n ** 2 } # => [1, 4, 9]
# Store a proc and reuse it as a block
square = Proc.new { |n| n ** 2 }
[1, 2, 3].map(&square) # => [1, 4, 9]
[4, 5, 6].map(&square) # => [16, 25, 36]
Method Objects as Callables
Ruby’s Method class is a third callable type — a bound method wrapped in an object. It has lambda-like argument checking and return behavior.
Example:
def double(n)
n * 2
end
m = method(:double)
m.call(5) # => 10
m.lambda? # => false (Method is not Proc)
[1, 2, 3].map(&m) # => [2, 4, 6]
# Useful with built-in methods
["1", "2", "3"].map(&method(:Integer)) # => [1, 2, 3]
[nil, 1, false, 2].select(&method(:itself)) # doesn't work — use filter_map
Pro-Tip: When you see
Proc.newin code that stores callables for later use, treat it as a warning sign. Thereturn-from-enclosing-method behavior means a Proc that returns becomes unsafe the moment the enclosing method exits. In any situation where you’re storing a callable object to call outside its creation context — configuration hooks, event handlers, pipeline steps — use a lambda. The argument-checking strictness is a bonus; the safe return semantics are the real reason.
Conclusion
Procs and lambdas differ in exactly two ways: arity enforcement and return semantics. Lambdas behave like methods — strict about arguments, returning from themselves. Procs behave like blocks — lenient about arguments, returning from their enclosing method. Understanding this distinction prevents the subtle bugs that come from using the wrong callable in the wrong context, and it makes the choice between them mechanical once you know where the callable will be called.
FAQs
Q1: Are lambdas faster than Procs in Ruby?
The performance difference is negligible for most code. Both are heap-allocated objects. The choice between them should be driven by semantics, not performance.
Q2: What does &method(:foo) do exactly?
method(:foo) returns a Method object wrapping the foo method. The & prefix converts any object that responds to to_proc into a block. Method defines to_proc, so &method(:foo) passes foo as a block to the receiving method.
Q3: Can a lambda have default argument values?
Yes. Lambda syntax supports the same argument features as methods: defaults, splats, keyword arguments, and double splats. add = ->(a, b = 1) { a + b }; add.(5) returns 6.
Q4: What’s the difference between Proc.new and proc { }?
In modern Ruby (2.x+), they’re identical. Both create a non-lambda Proc. Historically proc { } created a lambda in older versions; that inconsistency was fixed.
Q5: Can I check at runtime whether a callable is a lambda?
Yes. callable.lambda? returns true for lambdas and false for Procs. callable.class returns Proc for both, so .lambda? is the only reliable way to distinguish them.
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀