rubyguides

Kernel#proc

proc { |params| block } -> Proc

Kernel#proc creates a new Proc object by capturing the current block of code. Procs are closures — they retain access to variables from their defining scope even when called elsewhere. This makes them powerful for deferred execution, callbacks, and dynamic code organization.

For most Ruby code, the value of a proc is that it packages a block into something you can store and pass around. That gives you a small, reusable unit of behavior without having to name a whole class or method for every callback.

Syntax

proc { |params| block }
Proc.new { |params| block }

The shorthand proc { } is equivalent to Proc.new { }. Both create a Proc object that can be stored in variables, passed to methods, or called later.

In practice, you will usually see the shorthand when the code is already inside a block-heavy method. The longer form is still useful when you want the constructor call to stand out more clearly.

Parameters

ParameterTypeDefaultDescription
blockblockThe block of code to capture as a Proc object

Examples

Basic usage

my_proc = proc { |x| x * 2 }

my_proc.call(5)
# => 10

my_proc.call(21)
# => 42

This is the simplest shape of a proc example, and it shows the core idea quickly: one block of behavior can be stored once and reused with different inputs. That reuse is the whole reason procs are so handy in small callback-heavy methods.

Storing a proc in a variable

greeting = proc { |name| "Hello, #{name}!" }

greeting.call("Alice")
# => "Hello, Alice!"

greeting.call("Bob")
# => "Hello, Bob!"

Keeping the proc in a variable makes the reuse story even clearer. Instead of repeating the block inline, you give the behavior a name and call it whenever that same logic is needed. The named reference also helps when the same transformation appears in several places, since a single variable update fixes all call sites at once.

Passing a proc to a method

def apply_operation(array, operation)
  array.map(&operation)
end

double = proc { |n| n * 2 }
apply_operation([1, 2, 3], double)
# => [2, 4, 6]

Once a proc is being passed around, the main benefit is that the receiving method does not need to know where the behavior came from. It just receives a callable object and uses it like any other block.

The & operator converts a Proc to a block. This is covered in more detail in the Common Patterns section.

That conversion is what lets a proc fit naturally into APIs that expect a block. Instead of rewriting the logic, you can reuse the same callable object in both block and non-block forms.

Common Patterns

Converting between procs and blocks

Use & to convert a Proc to a block when passing to methods:

my_proc = proc { |x| x + 1 }

[1, 2, 3].map(&my_proc)
# => [2, 3, 4]

This form is common in Ruby because it keeps the call site compact while still preserving the behavior in a reusable object. The code stays short, but the intent is still obvious.

Use .to_proc on symbols for quick conversions:

[:upcase, :downcase, :reverse].map(&:to_s)
# => ["UPCASE", "downcase", "REVERSED"]

That shorthand is convenient when the transformation is already named by the method itself. It is a good fit for tiny data-shaping steps where a full block would not add any real clarity.

Symbol-to-proc shorthand is one of the small Ruby conveniences that keeps examples compact. It works well when the operation is simple and the method name itself is the clearest expression of intent.

Proc vs Lambda

Procs and lambdas are both Proc objects, but they differ in argument handling:

my_proc = proc { |a, b| [a, b] }
my_lambda = lambda { |a, b| [a, b] }

my_proc.call(1, 2, 3)
# => [1, 2]  - extra args ignored

my_lambda.call(1, 2, 3)
# => ArgumentError (lambda is strict)

The return behavior is where the gap between procs and lambdas becomes most visible in practice. A lambda’s return exits only the lambda body and the enclosing method continues normally afterward. A proc’s return, however, exits the entire calling method as though the return statement were written directly in the method. This difference can produce surprising results if you treat a proc like a lambda, so testing the return path is worthwhile when the two might be used interchangeably:

def test_proc
  p = proc { return "returned from proc" }
  p.call
  "this won't print"
end

def test_lambda
  l = lambda { return "returned from lambda" }
  l.call
  "this will print"
end

test_proc
# => "returned from proc"

test_lambda
# => "this will print"

Looking at the two return values side by side makes the structural difference hard to miss. It shows why procs and lambdas are related, but not interchangeable in code that depends on return flow.

This difference is one of the main reasons people reach for lambda when they need stricter control. Procs are more forgiving, but that flexibility also means they can surprise you if you expect method-like return behavior.

Using procs for configuration

Procs are excellent for configurable behavior:

class Calculator
  def initialize(&operation)
    @operation = operation
  end

  def calculate(values)
    values.map(&@operation)
  end
end

add_one = Calculator.new { |x| x + 1 }
add_one.calculate([1, 2, 3])
# => [2, 3, 4]

square = Calculator.new { |x| x ** 2 }
square.calculate([1, 2, 3])
# => [1, 4, 9]

This pattern is often easier to read than a custom setter or a large conditional branch. The object stays small, and the behavior is still flexible enough to vary by call site.

The pattern works well when you want a small object to accept a behavior at construction time. It keeps the calculation logic flexible without forcing the caller to subclass or monkey-patch anything.

Memoization with procs

def expensive_computation
  @cache ||= proc { heavy_calculation }
  @cache.call
end

def heavy_calculation
  puts "Computing..."
  42
end

expensive_computation
# Computing...
# => 42

expensive_computation
# => 42  (cached, no recomputation)

Here the proc is mostly acting as a stored unit of work. That makes the lazy setup easy to keep in one place, instead of scattering the computation logic across several methods.

Memoizing the proc itself is useful when the expensive work should stay behind a stable interface. The caller just invokes the method, while the internal block handles the lazy setup.

Errors

ArgumentError

Lambdas raise ArgumentError when passed the wrong number of arguments. Procs are lenient and assign nil to missing parameters or ignore extras:

strict = lambda { |a, b| a + b }
strict.call(1)
# => ArgumentError: wrong number of arguments (given 1, expected 2)

lenient = proc { |a, b| a + b }
lenient.call(1)
# => 1  (b is nil)

That last example is the practical warning sign: lenient argument handling can be convenient, but it also means missing parameters can slip through without an immediate exception. If the shape of the data matters, the stricter lambda form may be the better fit.

That lenient behavior is convenient, but it also means you need to be careful with arity expectations. When the block body depends on several parameters, a missing argument can turn into a subtle nil value instead of an immediate error.

See Also

  • lambda — creates a lambda Proc with strict argument checking
  • block_given? — checks if a block was passed to the current method