Procs

A Proc is Ruby’s way of turning a block of code into a full, storable, passable object — something you can save in a variable, hand off to a method, and call whenever you’re ready. Where a plain block passed to a method vanishes the moment that method returns, a Proc sticks around: you can call it immediately, later, or many times over, and it still remembers the environment it was created in. Understanding Proc is the foundation for understanding lambdas too, since a lambda is really just a Proc with two special behaviors switched on.

Overview: How Procs Work

In Ruby, almost everything is an object — even nil is an instance of NilClass, and 1.class really does return Integer. A block, on the other hand, is not an object. When you write [1, 2, 3].each { |n| puts n }, that { |n| puts n } is just syntax attached to the method call — you cannot store it in a variable or call it directly. A Proc fixes that: it wraps a block of code in a real object, an instance of the Proc class, so it gains all the abilities any object has — you can assign it, pass it as an argument, return it from a method, and call it whenever you like.

You create a Proc two equivalent ways: Proc.new { |args| ... } or the shorthand Kernel#proc { |args| ... }. Both produce an object with lenient, forgiving behavior around arguments and control flow, which is the classic “proc-style” semantics as opposed to the stricter “lambda-style” semantics you get from lambda { ... } or the stabby arrow ->. Under the hood, a lambda actually is a Proc — calling .class on one returns Proc — but it carries an internal flag, checked via .lambda?, that changes exactly two things: how strictly it checks the number of arguments it receives, and what return does inside it. Everything else — closures, calling syntax, storing it in a variable — behaves identically for both.

The single most important thing a Proc does is close over its surrounding scope. This is called a closure: when you create a Proc inside a method, it captures the local variables that are in scope at that moment, by reference, not by value. That means the Proc can read and mutate those variables even after the method that defined them has returned — the binding stays alive as long as something holds a reference to the Proc. This is what lets you build things like counters, memoized calculators, and configurable callbacks out of a few lines of code.

Syntax

There are a few interchangeable ways to create and call a Proc. The table below covers the essentials.

Form What it does
Proc.new { |x| x * 2 } Explicit constructor call; the block becomes the proc’s body. Lenient arity, return exits the enclosing method.
proc { |x| x * 2 } Kernel#proc, a shorthand for Proc.new with identical (proc-style) semantics.
->(x) { x * 2 } The “stabby lambda” literal. Produces a Proc object with lambda? true — strict arity, local return.
lambda { |x| x * 2 } Kernel#lambda, an alternate lambda syntax, equivalent to ->.

Once you have a Proc object, there are three interchangeable ways to invoke it, all shown in the first example below: my_proc.call(args), the shorthand my_proc.(args), and the array-style my_proc[args]. Any parameters declared between the pipes (|a, b|) work like ordinary method parameters, including support for defaults and splats.

Examples

Example 1: Creating and calling a Proc

greeter = Proc.new { |name| "Hello, #{name}!" }
puts greeter.call("Ada")
puts greeter.("Grace")
puts greeter["Alan"]

Output:

Hello, Ada!
Hello, Grace!
Hello, Alan!

All three lines call the exact same Proc object, just with different syntax — .call, the dot-parenthesis shorthand, and square brackets are all synonyms. Because a Proc is a real object, greeter can be passed around and reused as many times as needed, unlike a bare block.

Example 2: Lenient arity

add = proc { |a, b| (a || 0) + (b || 0) }
puts add.call(2, 3)
puts add.call(5)
puts add.call(1, 2, 3)

Output:

5
5
3

Notice that add.call(5) doesn’t raise an error even though the block expects two parameters — Ruby simply assigns b the value nil. And add.call(1, 2, 3) doesn’t complain about the extra third argument; it’s silently discarded. This is the defining trait of proc-style arity: procs are forgiving about argument count, while lambdas (shown next) are strict.

Example 3: Proc vs. lambda return semantics

def use_lambda
  l = lambda { return 10 }
  l.call
  20
end

def use_proc
  p = Proc.new { return 10 }
  p.call
  20
end

puts use_lambda
puts use_proc

Output:

20
10

This is the single most important behavioral difference between the two. Inside use_lambda, the lambda’s return only exits the lambda itself — control comes right back to use_lambda, which then evaluates 20 and returns that. But inside use_proc, the Proc‘s return exits the entire enclosing method immediately — the line 20 is never reached, and use_proc returns 10. A Proc doesn’t have its own “local” return; it borrows the return of whatever method it was created inside.

How It Works Step by Step

The closure behavior is easiest to see by watching a Proc outlive the method that created it:

def counter
  count = 0
  increment = Proc.new { count += 1 }
  increment
end

tick = counter
puts tick.call
puts tick.call
puts tick.call

Output:

1
2
3
  1. counter runs, creating a local variable count set to 0.
  2. The Proc.new { count += 1 } expression builds a Proc object. At this moment, Ruby attaches the current binding — including the local variable count — to that object. This is the closure.
  3. counter returns the Proc object itself (stored in increment), and the method call ends. Normally count would be garbage collected once its method finishes, but because the Proc still references it, it survives.
  4. tick now holds that same Proc object, outside of counter entirely.
  5. Each tick.call re-enters the block body, reads the still-alive count, adds 1, reassigns it, and — because count += 1 is the last expression evaluated — returns the new value.

This is exactly how you’d build a counter, an accumulator, or a simple memoizing cache without a class: the Proc carries its own private state around with it.

Common Mistakes

Mistake 1: Using return in a Proc with no enclosing method

Because a Proc‘s return tries to exit the method it was defined in, calling one at the top level of a script — where there is no enclosing method — blows up:

greeter = Proc.new { |name| return "Hi #{name}" }
puts greeter.call("Ada")

Output:

LocalJumpError: unexpected return

The fix is either to avoid an explicit return and rely on the block’s last expression as the value (procs, like all Ruby blocks and methods, return whatever their last line evaluates to), or to make sure the Proc is only ever called from inside a method:

def greet(name)
  greeter = Proc.new { "Hi #{name}" }
  greeter.call
end

puts greet("Ada")

Output:

Hi Ada

Mistake 2: Letting a Proc’s lenient arity hide a bug

Because procs don’t enforce argument count, a caller who forgets an argument doesn’t get an error — they get a quietly broken result:

process = Proc.new { |status, message| "#{status}: #{message}" }
puts process.call("ok")

Output:

ok: 

The caller almost certainly meant to pass a message too, but instead of an error pointing at the bug, message silently became nil, which interpolates as an empty string. When you want Ruby to catch this kind of mistake immediately, use a lambda instead — its strict arity checking raises ArgumentError the instant a call has the wrong number of arguments, right where the mistake happened:

process = ->(status, message) { "#{status}: #{message}" }
puts process.call("ok", "All good")

Output:

ok: All good

Called correctly it works the same as the proc version, but if a future caller forgets the second argument, this version fails loudly instead of producing a subtly wrong string.

Best Practices

  • Default to a lambda (->) for small, self-contained pieces of behavior — strict arity and a local return make lambdas behave much more like ordinary methods, which is usually what you want.
  • Reach for Proc.new or proc specifically when you want lenient arity, or when you’re capturing an incoming block via &block in a method signature.
  • Never rely on return inside a Proc unless you deliberately want to exit the enclosing method — it’s the most common source of surprising early exits.
  • Use the &:method_name shorthand (like &:upcase), which relies on Symbol#to_proc, instead of writing out a full Proc for simple one-method transformations.
  • Remember that a Proc is a closure: if it captures a mutable object like an array or hash from its surrounding scope, calling the proc can mutate that shared object — be intentional about what state you’re capturing.
  • Check .lambda? on a Proc object if you ever need to branch behavior based on whether it’s lambda-style or proc-style at runtime.
  • Don’t lean on a proc’s lenient arity to paper over careless calling code — it hides bugs instead of surfacing them.

Practice Exercises

  • Write a Proc called shout that takes a string and returns it upper-cased with an exclamation point appended (for example, "hi" becomes "HI!"). Call it once using .call, once using .(), and once using [].
  • Write a method make_multiplier(factor) that returns a Proc which multiplies its single argument by factor. Create double = make_multiplier(2) and triple = make_multiplier(3), then call each on 5. Expected output: 10 then 15.
  • Without running it, work out whether this raises an error and why: def run; p = Proc.new { return 42 }; p.call; end; puts run. Hint: think carefully about where the enclosing method boundary actually is.

Summary

  • A Proc wraps a block of code as a genuine object you can store in a variable, pass around, and call whenever you like.
  • Create one with Proc.new { ... } or the equivalent proc { ... } shorthand.
  • Procs are closures — they capture and can mutate the local variables from the scope where they were created, even after that scope’s method has returned.
  • Procs have lenient arity: missing arguments become nil, and extra arguments are silently discarded.
  • A lambda is technically a Proc too, but with strict arity checking and a return that only exits the lambda itself, rather than the enclosing method.
  • Call a proc with .call(args), .(args), or [args] — all three are equivalent.
  • Prefer lambdas for predictable, self-contained behavior; reach for Proc.new when you specifically need leniency or are capturing an incoming block.