Working with Procs and Lambdas: A Ruby Tutorial
Working with procs and lambdas in Ruby means learning to treat behavior as a value you can pass around, store, and call later. Both are callable blocks of code that share the Proc class, but they differ in two important ways: argument checking and return behavior.
A proc bends the rules to keep going; a lambda enforces the rules and surfaces problems early. Those two differences drive every practical decision about which one to use, and this tutorial walks through both with real examples.
The short version of the difference: a Proc is forgiving and a Lambda is strict. Both are instances of the Proc class, but the lambda variant checks arity and treats return like a method return, while a plain proc happily accepts the wrong number of arguments and returns from the enclosing method when you write return inside it. Those two differences sound minor, but they are the source of nearly every confusing bug people hit in the topic, so the bulk of this tutorial walks through real examples that show why each behavior was chosen and how to pick between them.
We’ll also touch on the & operator that converts blocks to procs and back, the Method#to_proc shortcut behind &:method_name, and a recipe for currying. For related foundations, see the Ruby symbols deep dive and the define_method tutorial.
Intro context
Ruby’s block syntax is usually enough for small methods, but it starts to feel cramped when you need to store behavior, pass it through several layers, or reuse it in more than one place. That is where procs and lambdas become useful. They let you treat behavior as a value, which means you can place it in arrays, return it from methods, or inject it into objects that need a callback.
The idea is not to replace blocks everywhere. Most Ruby code reads better when it keeps the block at the call site and uses a proc or lambda only when the behavior needs a name, a variable, or a longer lifespan. That distinction matters because it keeps the code readable while still giving you an escape hatch for the cases where a block by itself is too limited.
Another useful mental model is to think of procs as flexible closures and lambdas as small method objects. A closure remembers the surrounding variables, so both forms can capture state without global variables or instance variables. The difference is that lambdas act a little more like method calls, which makes them easier to reason about when you care about argument counts and predictable return flow.
What is a proc?
A proc is an instance of the Proc class that represents a block of code. You can store it in a variable, pass it around, and call it later.
That sounds simple, but it solves a common Ruby problem: sometimes you want a reusable chunk of behavior without wrapping it in a full class. A proc gives you that chunk. It can close over local variables, so it also remembers context from the moment it was created. That makes it practical for callback-heavy code, small DSLs, and helper objects that need to defer work until later.
Creating a proc
There are two ways to create a proc:
# Using Proc.new
my_proc = Proc.new { |name| puts "Hello, #{name}!" }
# Using proc (Ruby 1.8.7+)
my_proc = proc { |name| puts "Hello, #{name}!" }
Calling a proc
When you call a proc, Ruby executes the code inside it and returns the result of the final expression unless the proc uses return earlier. That return behavior is one of the big reasons procs feel different from methods and lambdas. It is also why procs can surprise people the first time they use them inside a method that expects the rest of the method body to keep running.
my_proc.call("Alice")
# => Hello, Alice!
# Alternative calling syntax
my_proc.()
my_proc===
What is a lambda?
A lambda is also a Proc object, but with some different characteristics. It behaves more like a method.
That method-like behavior is the main reason many Rubyists reach for lambdas when they want a small unit of logic that must behave predictably. A lambda wants the right number of arguments, and it returns only from itself. Those constraints make it easier to compose with other code because the lambda will not accidentally short-circuit a surrounding method.
Creating a lambda
# Using the lambda keyword
my_lambda = lambda { |name| puts "Hello, #{name}!" }
# Using the stabby lambda syntax (Ruby 1.9+)
my_lambda = ->(name) { puts "Hello, #{name}!" }
Calling a lambda
Calling a lambda looks the same as calling a proc, but the expectations are stricter. If you pass the wrong number of arguments, Ruby raises an error instead of silently continuing. That extra feedback is useful when the callable is part of a data pipeline or a background task, because you catch the problem where it starts instead of debugging it later in the chain.
my_lambda.call("Bob")
# => Hello, Bob!
# Alternative calling syntax
my_lambda.()
Key differences
Understanding the differences between procs and lambdas is essential for writing idiomatic Ruby code.
The easiest way to remember the distinction is this: procs are forgiving, lambdas are disciplined. Forgiving behavior is helpful when you are building a DSL, wiring together small callbacks, or moving a block through several layers of code. Disciplined behavior is helpful when you want something to act like a tiny method and fail fast if the call site is wrong.
1. argument handling (arity)
Lambdas are strict about arguments, while procs are lenient:
# Proc: extra arguments are ignored, missing arguments become nil
my_proc = proc { |a, b| puts "a: #{a}, b: #{b}" }
my_proc.call(1)
# => a: 1, b:
my_proc.call(1, 2, 3)
# => a: 1, b: 2
# Lambda: raises ArgumentError for wrong number of arguments
my_lambda = ->(a, b) { puts "a: #{a}, b: #{b}" }
my_lambda.call(1)
# => ArgumentError (wrong number of arguments (given 1, expected 2))
my_lambda.call(1, 2, 3)
# => ArgumentError (wrong number of arguments (given 3, expected 2))
2. return behavior
The return difference matters even more than arity because it changes control flow. A proc can jump out of the method that created it, which is occasionally useful but easy to misuse. A lambda returns to the caller that invoked it, so it behaves much more like a normal method call and keeps the surrounding method in control.
This is the most important difference:
# Proc: returns from the enclosing method
def method_with_proc
my_proc = proc { return "returned from proc" }
result = my_proc.call
puts "This line never runs"
result
end
method_with_proc
# => "returned from proc"
# Lambda: returns from itself, not the enclosing method
def method_with_lambda
my_lambda = -> { return "returned from lambda" }
result = my_lambda.call
puts "This line runs!"
result
end
method_with_lambda
# => This line runs!
# => "returned from lambda"
When to use each
Use procs when:
- You need flexible argument handling
- You want the return behavior to exit the enclosing method
- You’re building DSLs or configuration blocks
Those situations usually share one trait: the callback is more important than the strict shape of the call. If your code is trying to mirror natural language or define a small configuration language, a proc can keep the syntax light. It is also handy when the surrounding method should stop as soon as the proc decides it is done.
# Example: Configuration block
class Service
def configure(&block)
config = Config.new
block.call(config) if block_given?
config
end
end
Use lambdas when:
- You want strict argument checking
- You need behavior that behaves like a method
- You’re passing callbacks that shouldn’t affect the calling code
Those situations usually involve predictable data flow. A lambda is a better fit when the callback is one step in a larger chain, because the stricter arity and normal return behavior keep the chain honest. That predictability matters in code that is called from tests, jobs, or service objects, where a quiet failure would be harder to spot than an immediate exception.
# Example: Data transformation pipeline
transformations = [
->(x) { x.strip },
->(x) { x.upcase },
->(x) { x.reverse }
]
text = " Hello World "
transformations.each { |t| text = t.call(text) }
puts text
# => DLRO OLLEH
Practical examples
Example 1: custom array operations
This example shows a common pattern: a proc is used for a filter-like operation, while a lambda is used for a transformation. The point is not that one of these forms is always better. The point is that each one communicates a different level of strictness, and that hint is useful when you come back to the code later.
# Using a proc for flexible filtering
filter_proc = proc { |item| item.is_a?(String) && item.length > 3 }
items = [1, "hi", "hello", 42, "world"]
filtered = items.select(&filter_proc)
# => ["hello", "world"]
# Using a lambda for transformation
upcase_lambda = ->(s) { s.upcase }
upcased = items.select(&:is_a?).map(&upcase_lambda)
# => ["HI", "HELLO", "WORLD"]
Example 2: building a simple strategy pattern
Strategy objects do not have to be full classes. A lambda or proc can act as a lightweight strategy when the behavior is small and the surrounding class only needs a single callable. That is a nice middle ground between hard-coding a branch and creating a whole hierarchy of subclasses.
class PaymentProcessor
def initialize(strategy)
@strategy = strategy
end
def pay(amount)
@strategy.call(amount)
end
end
credit_card_strategy = ->(amount) { "Charging #{amount} to credit card" }
paypal_strategy = lambda { |amount| "Paying #{amount} via PayPal" }
processor = PaymentProcessor.new(credit_card_strategy)
puts processor.pay(100)
# => Charging 100 to credit card
processor = PaymentProcessor.new(paypal_strategy)
puts processor.pay(50)
# => Paying 50 via PayPal
Example 3: delayed execution
Deferred execution is another place where callable objects shine. The method below captures the block and runs it later on a background thread. This is a simple example, but the same shape appears in job runners, retry wrappers, and instrumentation hooks.
def create_delayed_task(delay, &task)
Thread.new do
sleep(delay)
task.call
end
end
task = create_delayed_task(1) { puts "Task executed after 1 second!" }
puts "Task scheduled"
task.join
# => Task scheduled
# => Task executed after 1 second!
Converting between blocks, procs, and lambdas
Once you are comfortable with the two forms, the next step is learning how Ruby converts between them. That conversion path is important because many APIs accept blocks, but you may prefer to build a proc or lambda first and then hand it to the API only at the final call site.
# Block to proc
def my_method(&block)
block # This is now a Proc
end
# Proc to block
my_proc = proc { |x| puts x }
# Use & to convert back to block
[1, 2, 3].each(&my_proc)
# Lambda to proc
my_lambda = ->(x) { puts x }
my_proc = my_lambda.to_proc
# Proc to lambda (risky - changes behavior)
my_proc = proc { |x, y| puts x + y }
my_lambda = my_proc.lambda? ? my_proc : my_proc.clone
Common pitfalls
The biggest mistakes in this topic are not syntax problems. They are control-flow problems. A small callable can look harmless, but if it exits a method too early or accepts the wrong arguments, the bug often shows up far from the source. These pitfalls are worth checking any time you refactor a block into a proc or lambda.
Pitfall 1: forgetting the return difference
# Wrong: Using lambda return in a loop context
def process_items(items)
items.each do |item|
next lambda { return } # This doesn't work as expected!
end
end
# Right: Use proc for early returns
def process_items(items)
items.each do |item|
next proc { return }.call
end
end
Pitfall 2: confusing symbol#to_proc with lambdas
The shorthand created by &:method_name is convenient, but it is not the same thing as writing a lambda by hand. It creates a proc from a symbol and then turns that proc into a block. That detail matters when you are debugging arity or trying to explain why a collection helper behaves differently from your own callable.
# This creates a proc from a symbol, not a lambda
:upcase.to_proc
# => #<Proc:0x00007f9a8c2d4e20>
# Use lambda for explicit behavior
upcase_lambda = ->(s) { s.upcase }
["hello", "world"].map(&upcase_lambda)
# => ["HELLO", "WORLD"]
Summary
If you only remember one thing from this tutorial, remember this: choose a proc when you want flexibility and choose a lambda when you want method-like behavior. That rule of thumb will cover most day-to-day Ruby code and help you decide quickly without overthinking the distinction.
| Feature | Proc | Lambda |
|---|---|---|
| Creation | proc {} or Proc.new {} | lambda {} or ->() {} |
| Arguments | Flexible (ignores extra, nil for missing) | Strict (raises on mismatch) |
| Return | Returns from enclosing method | Returns from itself |
| Use case | DSLs, flexible callbacks | Data transformation, strategy pattern |
Procs and lambdas are essential tools in Ruby. Master them, and you’ll write more flexible and expressive code.
Forward link
The next tutorial explores the Enumerable module, where you will see how callables and iterators meet in everyday Ruby code. Understanding procs and lambdas first makes those collection methods easier to read because you can recognize when Ruby is yielding, when it is calling a stored block, and when it is transforming a callback into another callable object.
If you want to keep practicing before moving on, revisit the examples above and rewrite them with your own methods. Change the argument lists, swap a proc for a lambda, and observe how the error handling changes. That small exercise makes the differences stick much better than reading the rules alone.
See Also
- Ruby Blocks and Iterators — Foundation for understanding how blocks relate to procs
- Ruby Symbol vs String — Understanding
&:method_nameshorthand withSymbol#to_proc - Ruby Methods Fundamentals — How methods and callable objects compare
- Ruby define_method Tutorial — Dynamic method creation that complements procs and lambdas