
June 24, 2026
Why does a local variable survive after a method returns?
One of Ruby’s most elegant features is its support for closures. A lambda or proc can continue accessing local variables long after the method that created them has already returned.
def message_function str = "The quick brown fox" lambda do |animal| puts "#{str} jumps over the lazy #{animal}." endendfunc = message_functionfunc.call("dog")
Output:
The quick brown fox jumps over the lazy dog.
At first glance, this looks impossible.
The local variable str belongs to message_function, and that method has already finished executing. Normally, its stack frame should have disappeared.
So where is str coming from?
A normal method call
When Ruby calls a method, YARV creates a stack frame (rb_control_frame_t) containing:
- local variables
- arguments
- the instruction sequence (ISEQ)
- the Environment Pointer (EP)
Once the method returns, this stack frame is normally destroyed.
That means every local variable inside it disappears.
Stack+-------------------------+| message_function || str = "quick brown..." |+-------------------------+Method returns...Stack is removed.
If Ruby behaved this way for lambdas, every closure would immediately become invalid.
lambda changes everything
When Ruby executes:
lambda do puts strend
it notices that the block captures a local variable.
Instead of leaving the stack frame where it is, Ruby copies the current environment to the heap.
Internally, three important objects are created:
- rb_env_t: stores the captured environment.
- rb_proc_t: represents the Proc object.
- rb_block_t: stores the block’s bytecode and its environment pointer.
The original stack frame still exists, but the closure now owns a persistent copy living on the heap.
Conceptually:
Stack Heapmessage_function -----> rb_env_t str str ^ | rb_proc_t
The Proc object now points to an environment that won’t disappear when the method returns.
The hidden trick: Ruby rewrites the Environment Pointer
Here’s something that surprises many developers.
Suppose we modify the variable after calling lambda.
def message_function str = "The quick brown fox" func = lambda do puts str end str = "The sly brown fox" funcendmessage_function.call
Output:
The sly brown fox
Wait…
Didn’t Ruby already copy the stack when lambda was created?
Yes.
The important detail is that after creating the heap copy, Ruby redirects the current Environment Pointer (EP) to the heap.
From that moment on, every access to str including assignments goes through the heap copy instead of the original stack frame.
In other words:
Before lambdaEP --> StackAfter lambdaEP --> Heap
The stack frame effectively becomes a temporary wrapper around the persistent environment.
Why multiple lambdas share variables
Consider this example.
i = 0inc = lambda { i += 1; puts i }dec = lambda { i -= 1; puts i }inc.calldec.callinc.call
Output:
101
Both lambdas operate on the same variable.
Ruby doesn’t create two copies of the environment.
Instead, after the first heap copy exists, subsequent calls to lambda reuse the same rb_env_t.
That means every closure created within the same lexical scope shares the same captured variables.
This behavior makes closures feel natural while avoiding unnecessary memory allocations.
The Proc object is the closure
Although Ruby developers often talk about “lambdas” and “closures,” internally there’s no dedicated rb_lambda_t.
Both lambda and Proc.new create an rb_proc_t object.
The only significant difference is a boolean flag:
is_lambda = true
or
is_lambda = false
The Proc object contains:
- the block’s instruction sequence (ISEQ)
- the captured environment
- metadata describing how it should behave
A closure is simply a Proc object plus the environment it captured.
Why this matters
Understanding how Ruby implements closures explains several behaviors that otherwise seem mysterious:
- Why local variables survive after a method returns.
- Why modifying captured variables affects every closure sharing that scope.
- Why closures can maintain state without using instance variables.
- Why Binding, define_method, and instance_eval all rely on the same underlying concepts.
Many of Ruby’s metaprogramming features build directly on this mechanism.
Final Thoughts
Closures aren’t magic.
When Ruby detects that a block must outlive its method, it preserves the execution environment by moving it from the stack to the heap. The resulting Proc object carries both the executable code and the environment it needs, allowing local variables to remain alive long after their original stack frame has disappeared.
Once you understand this mechanism, many advanced Ruby features Binding, define_method, instance_eval, and even eval become much easier to reason about.
