Course outline

Ruby / Contracts: callables, evaluation, failures, and value models / Lesson 7

Control block, Proc, and lambda lifetimes

Read closure capture, argument handling, non-local return, and synchronous invocation before storing or reusing callbacks.

About 5 min readContent reviewed
On this page

Lesson goals

  • Distinguish lambda argument and return behavior from ordinary Proc behavior.
  • Specify how often a callback runs and who owns its captured state and lifetime.

Similar callable syntax can have different contracts

A Ruby block is supplied with a method call, while Proc is an object representing a block-like computation that can be stored and invoked. A lambda is a Proc with lambda-specific argument and control-flow behavior. The & syntax can convert between a block parameter and a Proc representation at a call boundary. None of these forms creates a thread, background job, or durable task. Before accepting a callback, state its inputs, invocation count, allowed effects, possible exceptions, and lifetime. A convenient closure is still application behavior that needs an accountable owner.

Choose strict or permissive argument handling deliberately

A lambda normally checks its arguments like a method, while an ordinary Proc can be lenient about missing or extra positional arguments. That leniency can turn a missing value into nil instead of immediately identifying the caller error. Keyword rules and destructuring add further detail, so inspect the exact signature rather than using a blanket statement that all blocks accept anything. The example shows one required lambda argument and two positional Proc parameters with only one supplied value. The output is reasoned from Ruby 3.3 semantics; no interpreter executed it.

ruby Lambda and ordinary Proc argument behavior
strict = ->(value) { value * 2 }
lenient = proc { |value, extra| [value, extra] }
puts strict.call(3)
p lenient.call(3)
puts strict.lambda?
puts lenient.lambda?
Expected output
6
[3, nil]
true
false

Expected output is 6, [3, nil], true, false. Supplying the wrong arity to strict would raise rather than behave like this lenient Proc; that failing call is not made.

A return can target a surrounding method

A return inside an ordinary Proc can return from the method in which that Proc was created, not merely from the callback call. A lambda return is local to the lambda. If a Proc attempts a non-local return after its defining method has already ended, it can raise LocalJumpError. This matters when a method stores a callback for later use rather than invoking it immediately. next and break also have context-sensitive control behavior; do not treat them as interchangeable with a method return. Keep callback boundaries small enough that a reader can identify their target.

Observe where execution continues

The two methods below differ only in the kind of callback and the handling of its result. The Proc return exits the surrounding method before its final expression. The lambda returns a value to its caller, allowing the method to build a second result. These methods use no I/O beyond printing and no external state. Do not infer that every callback should be a lambda: choose the protocol the API needs, then document it. Unexpected non-local return is a contract problem, not a reason to rescue every exception around an iterator.

ruby Non-local and local callback return
def via_proc
  callback = proc { return 'method-return' }
  callback.call
  'after'
end
def via_lambda
  callback = -> { return 'lambda-return' }
  value = callback.call
  "after:#{value}"
end
puts via_proc
puts via_lambda
Expected output
method-return
after:lambda-return

Expected output is method-return then after:lambda-return. The example is self-contained but unexecuted. It demonstrates control flow, not asynchronous callback scheduling.

Captured mutable variables retain a lifetime

A closure can observe later changes to a captured variable, so storing a predicate does not automatically capture a frozen creation-time value. Create a deliberate local snapshot when that is the required policy. Captured arrays and strings can also be shared through other references. Thread-safe use needs its own synchronization or ownership, not merely a Proc wrapper. Block attachment precedence differs between braces and do/end in some expressions; use parentheses or simpler calls when ambiguity could bind the block to the wrong method. Test immediate and delayed invocation under the actual API contract.

Common errors and fixes

A stored Proc raises LocalJumpError when returning.

Why it happens
Its non-local return target has already ended.
How to fix it
Use a local-return lambda or redesign the callback lifetime.
Prevent it next time
Review return targets whenever callbacks escape their defining method.

A missing callback argument becomes nil unnoticed.

Why it happens
Ordinary Proc positional leniency concealed the mismatch.
How to fix it
Use a strict callable contract or validate the supplied arguments.
Prevent it next time
Check callback arity and keyword semantics at integration boundaries.

A callback sees a threshold different from the expected snapshot.

Why it happens
It captured a variable whose value later changed.
How to fix it
Capture an intentional immutable value or document live-state semantics.
Prevent it next time
Include delayed invocation in closure checks.

Check your understanding

1. Can a Proc safely be called later merely because it is an object?

Show worked solution

Not without inspecting captured state and control-flow targets. A non-local return can outlive its defining method, and referenced resources or mutable values can be stale or shared. Choose a callable with the intended return behavior, define ownership and lifetime, and verify delayed use explicitly. Object storage does not manufacture a valid future execution context.

2. Why might braces versus do/end matter when passing a block through nested calls?

Show worked solution

Their binding precedence can associate the block with a different method than the reader intended. Use explicit parentheses or split the expression, then verify which method receives and invokes the block. Readability is part of the callback contract; a compact expression is not useful if it hides the owner or changes invocation count.

Key takeaways

  • Callable argument, return, capture, and lifetime rules must be read together.
  • A block or Proc is not automatically concurrent, durable, or isolated.

Official references