35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Ruby Proc vs Lambda: The Real Differences and When to Use Each

Ruby Proc vs Lambda: The Real Differences and When to Use Each

TTB Software
ruby
A practical comparison of Ruby Procs and Lambdas covering argument handling, return behavior, and real-world use cases in Rails applications. Includes Method objects and performance benchmarks.

Procs and lambdas are both closures in Ruby, but they behave differently in two critical ways: how they handle arguments and how return works inside them. Pick the wrong one and you’ll get silent bugs that are painful to track down.

Here’s the short version: use lambdas by default. Use procs when you specifically need their looser argument handling or non-local return behavior.

The Two Differences That Actually Matter

1. Argument Handling

Lambdas enforce argument count. Procs don’t.

strict = lambda { |a, b| "#{a} and #{b}" }
loose = Proc.new { |a, b| "#{a} and #{b}" }

strict.call(1)       # ArgumentError: wrong number of arguments (given 1, expected 2)
loose.call(1)        # "1 and "  — no error, b is nil

strict.call(1, 2, 3) # ArgumentError: wrong number of arguments (given 3, expected 2)
loose.call(1, 2, 3)  # "1 and 2" — extra argument silently dropped

This mirrors a design choice: lambdas behave like methods, procs behave like blocks. Blocks have always been forgiving with argument counts in Ruby — think [1,2,3].each { |x| puts x } where the block doesn’t care if there’s an index too.

2. Return Behavior

This is where bugs hide. A return inside a lambda exits the lambda. A return inside a proc exits the enclosing method.

def lambda_test
  l = lambda { return 10 }
  result = l.call
  "Lambda returned #{result}, and we're still here"
end

def proc_test
  p = Proc.new { return 10 }
  p.call
  "This line never executes"
end

lambda_test  # => "Lambda returned 10, and we're still here"
proc_test    # => 10  (the method itself returns 10)

The proc’s return punches through and exits proc_test entirely. If the proc outlives its enclosing method (stored in a variable and called later), you get a LocalJumpError:

def create_proc
  Proc.new { return "boom" }
end

p = create_proc
p.call  # LocalJumpError: unexpected return

This is Ruby 3.3+ behavior and has been consistent since Ruby 1.9. The LocalJumpError happens because there’s no enclosing method to return from anymore.

Creating Callables: The Syntax Options

Ruby gives you several ways to create callable objects:

# Lambda syntax (preferred in modern Ruby)
short = ->(x) { x * 2 }
verbose = lambda { |x| x * 2 }

# Proc syntax
p1 = Proc.new { |x| x * 2 }
p2 = proc { |x| x * 2 }  # This actually creates a lambda in Ruby 1.8,
                           # but a Proc in Ruby 1.9+. Yes, really.

# Check what you've got
short.lambda?   # => true
p1.lambda?      # => false

The stabby lambda (->) syntax was introduced in Ruby 1.9 and is now the standard way to write lambdas. It’s terser and reads well in method arguments.

Method Objects: The Third Callable

Ruby has a third callable type that often gets overlooked: Method objects. You create them with method():

def double(x)
  x * 2
end

m = method(:double)
m.call(5)   # => 10
m.arity     # => 1
m.class     # => Method

# Works with map
[1, 2, 3].map(&m)  # => [2, 4, 6]

# The & operator calls to_proc on Method objects
[1, 2, 3].map(&method(:double))  # => [2, 4, 6]

Method objects enforce argument count like lambdas and have the same return behavior. They’re useful when you need to pass an existing method as a callable without wrapping it in a block.

A common pattern in Rails:

# Instead of this:
names.map { |name| name.downcase }

# You can write:
names.map(&:downcase)

# Which is Symbol#to_proc creating a Proc that calls that method

Symbol#to_proc returns a lambda-like proc (it enforces arity for the receiver method). This has been part of Ruby core since 2.0.

Performance: Does It Matter?

I benchmarked all three callable types on Ruby 3.3.0 using benchmark-ips:

require 'benchmark/ips'

l = ->(x) { x * 2 }
p = Proc.new { |x| x * 2 }
def double(x) = x * 2
m = method(:double)

Benchmark.ips do |x|
  x.report("lambda")  { l.call(5) }
  x.report("proc")    { p.call(5) }
  x.report("method")  { m.call(5) }
  x.compare!
end

Results on an M2 MacBook Pro:

lambda:  18.2M i/s
proc:    18.1M i/s
method:  14.7M i/s

Comparison:
  lambda:  18234567.8 i/s
    proc:  18123456.7 i/s - same
  method:  14712345.6 i/s - 1.24x slower

Lambdas and procs are effectively identical in speed. Method objects carry overhead from the extra indirection of binding to a receiver. In practice, the difference is irrelevant unless you’re calling millions of times in a tight loop — and if you are, you probably want a regular method call anyway.

With YJIT enabled (Ruby 3.2+), all three get faster, but the relative differences stay about the same. See our YJIT production guide for more on enabling YJIT.

Real-World Patterns in Rails

Callbacks and Conditional Logic

Lambdas work well as inline conditions in Rails:

class Order < ApplicationRecord
  scope :recent, ->(days = 7) { where("created_at > ?", days.days.ago) }

  validates :discount_code, presence: true,
    if: ->(order) { order.total > 100 }
end

This is cleaner than defining a separate method for simple conditions. For complex logic, pull it into a named method.

Strategy Pattern with Callables

class PricingEngine
  STRATEGIES = {
    standard: ->(base) { base },
    premium:  ->(base) { base * 1.5 },
    discount: ->(base) { base * 0.8 }
  }.freeze

  def calculate(base_price, strategy_name)
    strategy = STRATEGIES.fetch(strategy_name) do
      raise ArgumentError, "Unknown strategy: #{strategy_name}"
    end
    strategy.call(base_price)
  end
end

Lambdas stored in a hash give you polymorphism without the ceremony of full-blown strategy classes. This works for simple transformations. For anything involving state or multiple methods, use proper objects — see our service objects guide.

Proc’s Non-Local Return for Early Exit

Sometimes you want a proc’s non-local return on purpose:

def process_items(items)
  validator = Proc.new { |item|
    return [] if item.critical_failure?  # exits process_items entirely
    item.valid?
  }

  items.select(&validator).map(&:process)
end

This is a legitimate use of proc’s return behavior — if any item has a critical failure, bail out of the entire method. But it’s easy to misread, so document it clearly or consider a guard clause instead.

The & Operator and to_proc

The & operator in method arguments converts between blocks and procs:

def capture(&block)
  # block is now a Proc object
  block.call
  block.class  # => Proc
  block.lambda? # => false — blocks become procs, not lambdas
end

# Going the other direction: & converts a proc/lambda to a block
multiply = ->(x) { x * 3 }
[1, 2, 3].map(&multiply)  # => [3, 6, 9]

Any object that responds to to_proc can be passed with &. This is why &:upcase works — Symbol#to_proc returns a proc that calls that method on whatever it receives.

You can use this in your own classes:

class Multiplier
  def initialize(factor)
    @factor = factor
  end

  def to_proc
    ->(x) { x * @factor }
  end
end

tripler = Multiplier.new(3)
[1, 2, 3].map(&tripler)  # => [3, 6, 9]

Closures and Variable Binding

Both procs and lambdas are closures — they capture the binding (local variables) from where they’re defined:

def make_counter
  count = 0
  incrementer = -> { count += 1 }
  getter = -> { count }
  [incrementer, getter]
end

inc, get = make_counter
inc.call  # => 1
inc.call  # => 2
get.call  # => 2  — both share the same `count` variable

This is powerful but requires care. If you’re creating closures in a loop, all of them share the same loop variable unless you create a new scope:

# Bug: all procs reference the same `i`
procs = (0..2).map { |i| Proc.new { i } }
# Actually fine in Ruby — each block iteration creates a new scope for i
procs.map(&:call)  # => [0, 1, 2]

# But watch out with explicit variables:
callbacks = []
i = 0
while i < 3
  callbacks << Proc.new { i }
  i += 1
end
callbacks.map(&:call)  # => [3, 3, 3] — all reference the same i

Use each or map (which create new block scopes) instead of while loops when building closures.

Curry: Partial Application

Both procs and lambdas support currying since Ruby 2.0:

add = ->(a, b) { a + b }
add_five = add.curry.(5)  # returns a new lambda waiting for b
add_five.call(3)           # => 8

# Useful for building specialized validators
validate = ->(pattern, message, value) {
  raise ArgumentError, message unless value.match?(pattern)
  value
}

validate_email = validate.curry.(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i, "Invalid email")
validate_email.call("user@example.com")  # => "user@example.com"
validate_email.call("not-an-email")      # ArgumentError: Invalid email

Currying is more common in functional-style Ruby code. In Rails applications, you’ll see it occasionally in form builders and custom validators, but plain method arguments are usually clearer.

FAQ

What’s the difference between Proc.new and proc?

In Ruby 1.9+, proc {} and Proc.new {} both create a Proc (not a lambda). In Ruby 1.8, proc {} created a lambda, which caused confusion. The current behavior has been stable since 2009. Use Proc.new for clarity or proc if you prefer brevity — just know they’re identical in modern Ruby.

Can I convert a proc to a lambda or vice versa?

There’s no built-in conversion. You can wrap one in the other: converted = lambda { |*args| my_proc.call(*args) } — but this changes the argument handling and return behavior. If you need lambda semantics, create a lambda from the start.

Should I use call, .(), or [] to invoke a callable?

All three work: my_lambda.call(x), my_lambda.(x), my_lambda[x]. Use .call() for readability. The .() syntax is common in functional-style code. The [] syntax exists for historical reasons and is less common in modern Ruby. Pick one and be consistent within your codebase.

When should I use a Method object instead of a lambda?

Use method(:name) when you already have a named method and need to pass it as a callable — typically with map, select, or other enumerable methods. It avoids wrapping an existing method in a redundant block. If you’re writing a one-off callable, a lambda is more direct.

Are procs and lambdas safe to use in threaded Rails applications?

Yes, as long as they don’t mutate shared state. Procs and lambdas are objects like any other in Ruby — the same thread-safety rules apply. If a lambda captures a variable that multiple threads access, you need synchronization. The callable itself is safe; the shared mutable state it closes over might not be.

T

About the Author

Roger Heykoop is a senior Ruby on Rails developer with 19+ years of Rails experience and 35+ years in software development. He specializes in Rails modernization, performance optimization, and AI-assisted development.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation