Type Systems

Closures

A function that remembers where it was born

A closure is a function bundled with a reference to the variables from the scope where it was defined, so it can read and mutate that captured environment long after the outer function has returned.

  • CapturesFree variables by reference
  • Scope ruleLexical (definition site)
  • LifetimeOutlives enclosing frame
  • StorageHeap-boxed environment
  • First describedLandin, 1964

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

The intuition: a function with a backpack

Picture an inner function as a person leaving a room. On the way out they grab a backpack containing every variable from the room they still care about. They can walk anywhere — into another module, into an event handler, into a callback that fires ten seconds later — and the backpack comes along. That backpack is the closure environment, and the function plus its backpack is a closure.

The reason this matters is that the room — the outer function's stack frame — is normally torn down the instant the outer function returns. Local variables live on the stack, and the stack unwinds. A closure is the language's promise that the specific variables the inner function captured will survive that teardown, moved somewhere safe so they can still be reached.

Peter Landin coined the term closure in 1964 in his paper on mechanically evaluating expressions, describing a value that pairs a lambda expression (the code) with an environment (the bindings of its free variables). Sixty years later, every mainstream language — JavaScript, Python, Rust, Swift, Go, C#, Kotlin — ships closures, and they are the engine behind callbacks, iterators, decorators, and most of functional programming.

The precise mechanism: free variables and lexical scope

To be exact about what gets captured, split a function's variables into three kinds:

  • Bound variables — its own parameters and locals. These are created fresh on every call.
  • Free variables — names the function uses but does not declare. These must be resolved against some outer scope.
  • Global variables — free variables that resolve all the way out at module or global scope.

A closure captures the free variables that resolve to an enclosing function's scope — the interesting middle case. Resolution follows lexical scope: a free variable binds to the nearest enclosing definition in the source text, decided where the function is written, not where it is called. This is the single most important property. It means you can read a function and know exactly what each name refers to without tracing the call stack.

Contrast with dynamic scope (early Lisp, Bash, Emacs Lisp variables), where a free variable resolves to whatever value was most recently set by a caller at runtime. Dynamic scope makes closures impossible to reason about, which is why nearly every modern language is lexically scoped.

Capture is almost always by reference: the closure does not snapshot the value of a free variable, it shares the same binding cell. If the outer scope or another closure mutates that cell, every closure sharing it observes the change. Two closures created in the same scope can therefore communicate through a shared private variable — the basis of the module pattern and of stateful generators.

How runtimes implement it: boxing and escape analysis

The cost story starts with a problem: locals live on the stack, but a closure needs them after the stack frame is gone. Runtimes solve this by boxing — moving a captured variable off the stack into a heap-allocated cell. The closure then holds a pointer to that cell.

A compiled closure value is a small heap record with two fields: a code pointer (which function to run) and an environment pointer (the captured cells, often called upvalues in Lua/Python-style VMs). Calling the closure loads the environment so the body can dereference its free variables.

Smart compilers run escape analysis to avoid the heap when they can prove a variable never outlives its frame. Go's compiler, for instance, will keep a captured variable on the stack if the closure provably does not escape the function — printed by go build -gcflags=-m as "moved to heap" or not. The numbers below make the cost concrete.

When closures are the right tool

  • Callbacks and event handlers — capture the context a callback will need when it eventually fires, without passing it through every layer.
  • Data privacy / the module pattern — variables captured in a closure are unreachable from outside, giving true private state before classes had private fields.
  • Function factories and partial application — return a specialized function that has "baked in" some arguments (a multiplier, a logger prefix, a configured fetcher).
  • Memoization and iterators — keep a private cache or a private cursor that persists across calls without a global.
  • Decorators — wrap a function in a closure that adds timing, retries, or auth around it.

Reach for a class instead when you need many methods sharing the same state, inheritance, or when the captured state is large and you want explicit control over its lifetime. A closure with one captured variable is cleaner than a class; a closure capturing a dozen is usually a class wearing a disguise.

Closures vs other ways to bundle code and state

ClosureObject / class instancePlain functionGlobal variablecurrying / partial app
Carries stateYes — captured envYes — fieldsNoYes — but shared by allYes — bound args
State is privateYes (unreachable from outside)Only with private fieldsN/ANoYes
Multiple "methods" on the stateAwkward (return an object of closures)NaturalNoNoNo
Per-instance allocation1 heap record + boxed cells1 object + vtable/protoNoneNone1 closure
Capture semanticsBy reference (shared binding)Explicit assignmentShared mutableBy reference
Risk of accidental sharingHigh (loop-variable bug)LowNoneVery highMedium
Typical useCallbacks, factories, iteratorsLong-lived domain entitiesPure transformsConfig, singletonsConfigured helpers

The deep duality: a closure is an object with one method, and an object is a closure with many. Both bundle behavior with state — they just optimize for different cardinalities. Smalltalk and Scheme communities have argued this point since the 1970s, and both encodings are formally equivalent.

What capture actually costs

  • A closure that captures nothing is free. V8, the JVM, and Go all special-case zero-capture functions — they compile to a single shared code pointer with no per-instance allocation, identical to a top-level function.
  • Each captured variable that escapes is one heap allocation. On a modern allocator that is roughly 15–30 ns plus the eventual GC cost; a tight loop creating a million capturing closures is the difference between a stack-only ~1 ms and a heap-bound ~30–50 ms run, dominated by allocation and collection.
  • Indirection on every free-variable read. A boxed variable lives behind a pointer, so reading it is a load-then-load instead of a single stack load — typically one extra L1 cache hit (~1–4 cycles) when hot, far more on a miss.
  • Leak surface equals the whole captured frame. A 10 MB array captured but never used still cannot be freed while the closure is reachable; the GC sees a live reference, not your intent.

JavaScript implementation

The canonical examples — a counter with private state, and a function factory:

// Private state: `count` is unreachable except through the returned closures.
function makeCounter() {
  let count = 0;                       // captured free variable
  return {
    inc() { return ++count; },         // both closures share the SAME binding
    dec() { return --count; },
    value() { return count; },
  };
}

const c = makeCounter();
c.inc(); c.inc(); c.dec();
console.log(c.value());                // 1 — no way to touch `count` directly

// Function factory — bakes in `factor`:
function multiplier(factor) {
  return x => x * factor;              // `factor` captured by reference
}
const triple = multiplier(3);
console.log(triple(10));               // 30

The famous bug — and its fix. Capturing a loop variable by reference means every closure reads the variable's final value:

// BUG: var has one binding shared across all iterations.
const bad = [];
for (var i = 0; i < 3; i++) bad.push(() => i);
console.log(bad.map(f => f()));        // [3, 3, 3] — all see final i

// FIX: let creates a fresh binding PER iteration (ES6 spec guarantees this).
const good = [];
for (let i = 0; i < 3; i++) good.push(() => i);
console.log(good.map(f => f()));       // [0, 1, 2]

Before let existed, the idiom was an IIFE — wrap the body in an immediately-invoked function so each iteration gets its own parameter j: (function(j){ arr.push(() => j); })(i);

Python implementation

Python closures behave the same way, with one extra wrinkle: to assign to a captured variable you must declare it nonlocal, otherwise Python treats the assignment as creating a new local.

def make_counter():
    count = 0                      # captured free variable
    def inc():
        nonlocal count             # WITHOUT this, `count += 1` raises UnboundLocalError
        count += 1
        return count
    return inc

c = make_counter()
print(c(), c(), c())               # 1 2 3

# Function factory:
def multiplier(factor):
    return lambda x: x * factor    # `factor` captured

triple = multiplier(3)
print(triple(10))                  # 30

# The same loop bug — late binding of the free variable `i`:
bad = [lambda: i for i in range(3)]
print([f() for f in bad])          # [2, 2, 2] — all see final i

# Fix: bind the current value as a default argument (evaluated at def-time).
good = [lambda i=i: i for i in range(3)]
print([f() for f in good])         # [0, 1, 2]

You can inspect what a Python closure captured directly: inc.__closure__ is a tuple of cell objects, and inc.__closure__[0].cell_contents reads the live captured value — concrete proof that capture is a shared cell, not a copy.

Variants worth knowing

Capture by value vs by reference. C++ lambdas let you choose explicitly: [=] copies captured variables into the closure, [&] captures references. By-value avoids the loop bug and dangling references but snapshots state at creation time. Rust's move closures similarly take ownership of captures.

Upvalues and open vs closed capture. Lua distinguishes open upvalues (still pointing at a live stack slot) from closed ones (boxed to the heap when the slot dies). The VM keeps capture on the stack as long as it can — a real-world version of escape analysis.

Closures as objects (and vice versa). Returning an object whose methods are all closures over the same private variables is the JavaScript module pattern. It is exactly the "closures and objects are dual" idea made practical.

Function pointers without capture. C has no closures; the workaround is a function pointer plus an explicit void* context argument — the environment passed by hand. Every callback-based C API (qsort's comparator, pthreads) reinvents the closure manually.

Defunctionalization. A compiler optimization (and a hand technique) that replaces closures with a tagged data structure plus a single dispatch function — turning higher-order code into first-order code. Used in whole-program compilers and proof assistants.

Common bugs and edge cases

  • The loop-variable trap. The number-one closure bug. Closures created in a loop all share one binding unless the language or you give each iteration its own. Use let (JS) or a default-argument bind (Python).
  • Mutation through a shared capture. Two closures over the same variable see each other's writes. Sometimes that is the feature (a counter); sometimes it is a surprise (a parser that shares an index it shouldn't).
  • Accidental retention / memory leak. A closure pins its entire captured environment. A click handler that captures a huge DOM-derived array keeps it alive for the life of the listener. Capture only what you need.
  • Python's assignment-makes-local rule. Forgetting nonlocal turns a mutation into a new local and raises UnboundLocalError.
  • Dangling captures in systems languages. A C++ [&] lambda that outlives the captured local is undefined behavior — a use-after-free waiting to happen. Rust's borrow checker rejects this at compile time.
  • Confusing capture time with call time. Capture is by reference, so the value read is the one present when the closure runs, not when it was created — unless you explicitly snapshot.

Frequently asked questions

What is the difference between a closure and a function?

A function is just code. A closure is that code plus a reference to the environment — the set of free variables it uses from the enclosing scope. Every closure is a function, but a function only becomes a closure when it captures at least one variable from an outer scope and outlives that scope.

Does a closure copy variables or reference them?

In most languages, closures capture variables by reference, not by value. The closure holds a pointer to the same binding the outer scope used, so if that variable changes later, the closure sees the new value. This is exactly what causes the classic loop-variable bug, where every closure created in a loop ends up reading the loop counter's final value.

Why do all my closures in a loop print the same value?

Because they all captured the same mutable loop variable by reference. By the time the closures run, the loop has finished and the variable holds its final value. The fix is to give each iteration its own binding — use let instead of var in JavaScript, or bind the value as a default argument or via a factory function in Python.

Do closures cause memory leaks?

They can. A closure keeps its entire captured environment alive as long as the closure itself is reachable. If a long-lived closure captures a large object it never uses, the garbage collector cannot free that object. Capturing only the specific values you need, or nulling references, avoids the leak.

How are closures implemented under the hood?

A closure is typically a heap-allocated record holding two things: a pointer to the function's code and a pointer to the captured environment (often called the closure environment or upvalue list). When the closure is called, the runtime loads that environment so the body can resolve its free variables. Variables that escape their stack frame are 'boxed' onto the heap so they survive after the frame returns.

What is a free variable in a closure?

A free variable is one that a function uses but does not declare itself — it is neither a parameter nor a local. The closure resolves free variables against the lexical environment where the function was defined, not where it is called. This lexical-scope rule is what makes closures predictable.