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 ArgumentError on a mismatch. A proc is forgiving: missing arguments become nil, and extra arguments are simply discarded.
  • return semantics. Calling return inside a lambda exits only the lambda, handing control back to whatever called it — just like returning from an ordinary method. Calling return inside a proc exits the enclosing method the proc was defined in, skipping everything after the point where the proc was called. If a proc containing return is called outside of any enclosing method, Ruby raises a LocalJumpError because 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 Proc object is created and assigned to my_proc. Nothing inside it runs yet — creating a proc just packages up the code.
  • my_proc.call executes the proc’s body. It hits return "returned from proc".
  • Because this proc is non-strict (not a lambda), its return is scoped to the method it was defined in — proc_return_demo — not just the proc. Ruby immediately unwinds out of proc_return_demo with 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_demo receives "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 predictable return behavior.
  • 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.new requires 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 given Proc object follows.
  • Prefer ->(x) { x * 2 } over lambda { |x| x * 2 } for short, self-contained lambdas; it’s the idiom modern Ruby style guides recommend.

Practice Exercises

  • Write a lambda called cube using the -> literal that returns its argument raised to the third power, and print cube.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 a Proc.new with an early return inside numbers.each to return the first even number in the array, or nil if 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 Proc class; a hidden flag, readable via lambda?, decides which rules they follow.
  • Lambdas check argument count strictly, raising ArgumentError on a mismatch; procs are lenient, padding missing arguments with nil and discarding extras.
  • return inside a lambda exits only the lambda; return inside 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.new always needs its own explicit block — it can’t implicitly borrow one from the enclosing method.