How Ruby Implements Closures: What Really Happens When You Call lambda

How Ruby Implements Closures: What Really Happens When You Call lambda
How Ruby Implements Closures: What Really Happens When You Call lambda

June 24, 2026

Why does a local variable survive after a method returns?

Tokyo Topographic Map
Built for Ruby on Rails

Build Maps Without
Google APIs

Generate beautiful production-ready maps directly from your Rails backend. Fast rendering, zero external dependencies, full control.

✓ No API fees ✓ Self-hosted ✓ Rails Native ✓ Fast Rendering
Why developers switch
Replace expensive map stacks.

Stop relying on third-party map billing and bloated JS libraries. Render static or dynamic maps directly in Ruby.

Try It Now
Tokyo MapView Demo

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}."
end
end
func = message_function
func.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 str
end

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 Heap
message_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"
func
end
message_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 lambda
EP --> Stack
After lambda
EP --> Heap

The stack frame effectively becomes a temporary wrapper around the persistent environment.


Why multiple lambdas share variables

Consider this example.

i = 0
inc = lambda { i += 1; puts i }
dec = lambda { i -= 1; puts i }
inc.call
dec.call
inc.call

Output:

1
0
1

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.

Article content

Leave a comment