Closures
Consider a function `makeCounter()`. It returns a new function each time it's called. That returned function increments and reports a private counter. Where does that counter live? This is after `makeCounter` has already returned and its stack frame is gone. This puzzle is a function outliving the scope that created it. Yet it still remembers that scope's variables. This is exactly the problem closures solve. Every modern language with first-class functions answers this question the same way. This includes JavaScript and Python, as well as OCaml, Haskell, and Rust. A function value is not just code. It is code paired with the environment it was defined in. Closures matter for a key reason. They are the mechanism underlying event handlers, callbacks, iterators, generators, memoization, and currying. They are also much of the machinery used to build higher-order functions into working software. Without closures, "pass a function as an argument" is a hollow feature. The passed function could only ever operate on global state. That's because it would have no way to remember the specific context in which it was created. Closures are what let a function returned from `makeAdder(5)` remember that `5`. This holds even though `5` was a local parameter of a call. That call has long since finished executing. Here is the big picture. Once you allow functions to be created and passed around dynamically, a function is really two things bundled together. These are a piece of code (the body) and an environment. The environment is a mapping from the free variables in that code to the bindings they refer to. Compilers and interpreters implement this bundle, literally called a closure. They use heap-allocated data structures that keep captured variables alive. These variables stay alive as long as any closure needs them. This is precisely why languages with closures typically need garbage collection. Alternatively, they need some equivalent lifetime-management discipline. This article gives a precise definition of closures in terms of lexical scoping and free variables. It walks through how an interpreter or compiler actually constructs and represents closure values. It analyzes the space and time costs of variable capture. It also works a full example by hand. This traces exactly which environment each closure carries. It explains why the classic "closures capture variables, not values" surprise happens in loops.
- A closure pairs a function's code with the environment (variable bindings) that was live where it was defined — this is called lexical scoping.
- Closures solve the problem of free variables: names used in a function body but not bound by its own parameters, which must be resolved to bindings from the enclosing scope.
- Implementations represent a closure as a heap record: a code pointer plus a captured environment (an array, linked frame chain, or explicit "environment record").
- Captured variables must outlive their original stack frame, so closures force heap allocation and interact directly with garbage collection.
- Closures capture variables (mutable storage locations), not their values at creation time — the classic loop-variable bug stems directly from this.
- Closures generalize into currying, memoization, iterators/generators, and are the conceptual seed for later ideas like lazy evaluation and monadic encapsulation of effects.
A closure is a function value implemented as code plus a captured environment, letting it use variables from its birthplace long after that birthplace's stack frame has disappeared.
The idea
In a language with lexical scoping (also called static scoping), the meaning of a variable reference is determined by where it appears textually in the source code, not by the dynamic call chain that leads to it. When a function `f` is defined inside another function `g`, and `f`'s body refers to a variable `x` that is a parameter or local of `g`, that reference to `x` is called a free variable of `f`: free because `f` does not bind it itself, it merely uses it, relying on the enclosing scope to supply its meaning. A closure is the runtime realization of this idea: a value consisting of (1) a pointer to the code of the function and (2) an environment, which supplies bindings for all of `f`'s free variables. Formally, if `f`'s code has free variables , then a closure for `f` is a pair
where is an environment mapping each to a location (or value) — precisely the bindings that were in scope at the point `f` was defined. Calling the closure means executing with extended by bindings for the actual parameters. This is what distinguishes closures from plain function pointers in C: a C function pointer is just the code address, because C has no nested functions with free variables to capture — every name a C function uses is either global or one of its own locals/parameters, resolved at compile time with no runtime environment needed. The key subtlety, and the source of most closure bugs, is that environments bind variables (mutable storage locations), not values. If a closure captures a variable `x` and later, before the closure is called, some other code mutates `x`, the closure sees the new value — because it holds a reference to the location, not a frozen snapshot. This is by design: it is what makes counters, accumulators, and mutable-state encapsulation with closures work at all. It also means closures interact deeply with variable lifetime: because a closure can be called long after its defining function returns, any variable it captures must be kept alive on the heap rather than reclaimed when the stack frame pops — this is the central implementation challenge discussed below, and it is the reason languages that support closures generally require automatic memory management or some substitute (Rust captures by move/borrow and uses ownership rules instead).
How it works
An interpreter or compiler must do two things: (1) at the point a function-valued expression (a lambda, `def`, or `function` literal) is evaluated, construct a closure value by capturing the current environment; (2) at the point a closure is called, extend the captured environment with parameter bindings and evaluate the body in it. Free-variable analysis, done once (statically, ahead of time) rather than repeated on every call, tells the implementation exactly which names must be captured, which is the key optimization over naively capturing "everything visible."
Step 1 — static free-variable analysis. For each function definition, compute the set of free variables: names referenced in the body that are not parameters and not locally bound (by `let`, inner `def`, etc.) within that body. This is a straightforward recursive syntax traversal.
Step 2 — closure creation at definition time. When the enclosing code reaches the lambda expression during evaluation, allocate a closure record on the heap containing a pointer to the compiled code and an environment that supplies bindings for exactly the free variables computed in Step 1 (in efficient implementations, only those, not the whole enclosing scope).
Step 3 — application. When the closure is called with arguments, build a new environment frame binding the formal parameters to the argument values, with that frame's parent (for further free-variable lookups) being the closure's captured environment — not the caller's environment. This is the defining feature of lexical scoping: variable lookup follows the chain of definition sites, never the chain of call sites (which would be dynamic scoping). ```text // Free-variable computation (syntax-directed) function FreeVars(expr, bound): match expr: case Var(x): if x in bound: return {} else: return {x} case Lambda(params, body): return FreeVars(body, bound ∪ params) case Apply(fn, args): return FreeVars(fn, bound) ∪ (∪ FreeVars(a, bound) for a in args) case Let(x, rhs, body): return FreeVars(rhs, bound) ∪ FreeVars(body, bound ∪ {x}) case Literal(_): return {} // Evaluation with closure creation and application function Eval(expr, env): match expr: case Literal(v): return v case Var(x): return env.lookup(x) // walk the environment chain case Lambda(params, body): fv = FreeVars(Lambda(params, body), {}) captured = env.restrict(fv) // only what's actually free — key optimization return Closure(code = body, params = params, env = captured) case Apply(fnExpr, argExprs): closure = Eval(fnExpr, env) args = [Eval(a, env) for a in argExprs] newEnv = Environment(parent = closure.env) for (p, a) in zip(closure.params, args): newEnv.bind(p, a) return Eval(closure.code, newEnv) case Let(x, rhs, body): v = Eval(rhs, env) newEnv = Environment(parent = env) newEnv.bind(x, v) return Eval(body, newEnv) ``` The crucial line is `newEnv = Environment(parent = closure.env)` inside `Apply` — the new call frame's parent is the environment stored in the closure, not `env` (the caller's environment). This single design choice is what makes scoping lexical rather than dynamic: a free variable inside the closure body always resolves relative to where the closure was written, regardless of who calls it or from where.
Complexity & correctness
Time. Constructing a closure takes time and space, where is the number of free variables it captures (not the size of the whole enclosing environment) — this is why free-variable analysis, precomputed once per lambda at compile time, matters: naively copying the entire enclosing environment on every closure creation would be far more expensive and unbounded in size. Calling a closure costs to build the new argument frame plus whatever the variable-lookup discipline costs. Variable lookup cost depends on the environment representation:
- Environment as a linked chain of frames (as in the pseudocode above): looking up a variable defined scopes outward costs in the worst case, since you may walk parent pointers. This is simple and correct but can be slow for deeply nested closures.
- Flattened/array-based environments with compile-time slot resolution: a compiler pass (lexical addressing) computes, for every variable reference, a pair (depth, index) once, ahead of time, so lookup at runtime is array indexing regardless of nesting depth. This is the technique used by efficient interpreters and compilers (e.g., the "De Bruijn index"–style compilation used in many functional-language implementations).
Space. Each closure occupies heap space for its captured environment plus a constant for the code pointer. If closures are created in a loop of length each capturing variables, total space is . Because closures can outlive the stack frame that created them, captured variables must be heap-allocated, not stack-allocated: a compiler doing escape analysis determines which locals are captured by an escaping closure and allocates only those on the heap (a well-known optimization — locals never captured by an escaping closure can stay on the stack, since they provably die when the frame pops).
Correctness — the escaping-reference invariant. The correctness property closures must satisfy is: > If a closure is created while a binding (variable at storage location ) is in scope, then every subsequent read of inside a later call to must observe the value currently stored at , exactly as if the enclosing scope's frame were still alive. The implementation obligation this imposes is: the storage location must not be reclaimed or reused while any live closure's environment still references it. This is exactly a reachability condition, which is why a tracing garbage collector (mark-and-sweep, generational, etc.) is a natural and common solution — a captured variable is reachable from a live closure, hence never collected while that closure exists, hence the invariant holds automatically as a corollary of GC correctness (an object is collected only when unreachable from any root). Languages without GC (C++, Rust) instead enforce the invariant at compile time: Rust's borrow checker statically rejects a closure that would outlive a captured reference, forcing you to capture by move (ownership transfer) instead, trading runtime cost for a compile-time proof of the same invariant.
Correctness of "capture the variable, not the value." Because maps to a location rather than a frozen value, two closures created in the same scope that both capture observe the same mutations to — this is exactly the semantics required to implement shared mutable state via closures (e.g., a counter's `increment` and `read` closures sharing one counter cell). This is not a bug; it is the whole reason closures can encapsulate private mutable state without any language-level "object" construct.
Tradeoff. The convenience of implicit capture has a cost: it is easy to accidentally capture a loop variable by reference when you meant to capture its value at a particular iteration (the classic bug below), and it is easy to accidentally keep a large object alive far longer than intended, simply because some long-lived closure captured a reference to it (a real cause of memory leaks in JavaScript event-handler code, for instance).
Worked example
Trace this program, in a JavaScript-like syntax with `let` (block-scoped, fresh binding per loop iteration — contrast with `var`, discussed in pitfalls): ```text function makeCounters() { let counters = []; for (let i = 0; i < 3; i = i + 1) { let count = 0; counters.push(function() { count = count + i; return count; }); } return counters; } let cs = makeCounters(); print(cs[1]()); // ? print(cs[1]()); // ? print(cs[0]()); // ? ``` Step through closure creation and calls explicitly:
| Step | Action | Environment captured / used | Result |
|---|---|---|---|
| loop iter `i=0` | `let count = 0` creates fresh cell ; closure created | `cs[0] = F_0` | |
| loop iter `i=1` | fresh cell ; closure created | `cs[1] = F_1` | |
| loop iter `i=2` | fresh cell ; closure created | `cs[2] = F_2` | |
| call `cs[1]()` | run 's body: `count = count + i` reads , , writes | uses only | returns 1 |
| call `cs[1]()` again | reads , , writes | same , mutated | returns 2 |
| call `cs[0]()` | reads , , writes | uses , untouched by the calls above | returns 0 |
Notice two things the trace makes explicit. First, each closure has its own environment record because `let` creates a fresh binding per iteration — this is why `cs[0]`, `cs[1]`, `cs[2]` don't interfere with each other, even though they were all created by the "same" lambda expression. Second, within one closure, state genuinely persists and mutates across calls: `cs[1]()` called twice returns `1` then `2`, because both calls share the same captured cell — this is the "shared mutable location, not copied value" behavior proven correct above. Now contrast with the classic bug: replace `let i` with `var i` (function-scoped, one shared binding for the entire loop, as in pre-ES6 JavaScript, or as if you'd hoisted a single mutable variable outside the loop in any language). Then there is only one cell for `i`, shared by all three closures:
| Step | Environment | Result |
|---|---|---|
| all three closures created | all point to the same | — |
| loop finishes | holds `3` | — |
| call `cs[0]()` | reads `i` from shared = 3 | returns `3`, not `0` |
This is not an implementation bug — it is the correct, faithfully-implemented consequence of variable capture-by-reference applied to a single shared binding. The fix (`let` instead of `var`, or in languages without per-iteration `let`, an explicit inner function to force a fresh binding per iteration) works precisely because it gives each closure a distinct location to capture, restoring the intended semantics.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Loop-variable capture bug | All iterations share one mutable binding (`var`-style scoping), so every closure reads the final value | Use per-iteration bindings (`let` in a `for`, or an IIFE/helper function to freshly bind each iteration) |
| "Closures copy values" misconception | Programmers assume capture-by-value; it's actually capture-by-reference to a location | Explicitly copy (`let x = i` inside the loop body) if a snapshot is truly wanted |
| Capturing too much, causing memory leaks | Naive implementations capture the entire enclosing environment instead of only free variables, keeping large unrelated objects alive | Compilers should perform free-variable analysis and capture only what's used; programmers should null out large captured references when done |
| Confusing closures with objects | Both encapsulate state + behavior, but closures expose only the functions you return, with no accidental field access | Use closures deliberately for encapsulation; use objects/classes when multiple methods must share rich mutable state with a public shape |
| Assuming closures are stack-allocated | Beginners write closure-based code as if it behaves like a C function pointer with automatic-storage locals | Understand that captured locals are promoted to the heap; in GC'd languages this is invisible but has real allocation cost |
| Mutable capture in concurrent code | Two closures sharing one mutable cell across threads without synchronization causes race conditions | Treat a captured shared cell exactly like any other shared mutable state: guard it with a lock, atomic, or channel |
Pioneers & history
The concept of lexical scoping and the closure as its implementation mechanism emerged from the confluence of the lambda calculus as a model of computation and the practical need, in the 1960s, to implement functions as first-class values in real interpreters.
Origins · 1 minPeter J. Landin and how this was discovered
Landin's SECD machine (Stack, Environment, Control, Dump) was the first formal computational model to make the environment component of a closure an explicit, first-class part of the machine state, rather than an informal side condition — this is the direct ancestor of every environment-based interpreter and virtual machine used to teach or implement closures today, including the pseudocode presented in this article.
Where this matters
Closures are the backbone of asynchronous and event-driven programming: JavaScript callbacks, Promise `.then()` handlers, and React component hooks (`useState`, `useEffect`) all rely on closures to remember which specific piece of state or context they belong to across asynchronous gaps in execution. Python's decorators, generators, and `functools.partial`-style currying are implemented via closures capturing the function or arguments being wrapped. In compilers themselves, closure conversion (the process of turning nested functions with free variables into flat top-level functions plus explicit environment structs) is a standard, load-bearing compilation pass for every functional language implementation — OCaml, Haskell (GHC), Scheme, Racket, and Erlang/Elixir's BEAM VM all perform it. In systems languages, Rust's closures (`Fn`, `FnMut`, `FnOnce` traits) make the capture-by-reference-versus-by-value distinction explicit and statically checked, showing that the closure/environment model generalizes even to languages built around manual memory safety rather than garbage collection. Closures also underlie memoization caches (a closure capturing a hash table alongside the function it wraps), iterator and generator protocols (a closure capturing "where I am" in a sequence), and are the conceptual stepping stone toward lazy evaluation (a thunk is essentially a closure that defers a computation) and toward monads (many monadic combinators, like `bind` for the state monad, are implemented as closures threading hidden state through a computation).
Ask the tutor: Can you show how closure conversion would rewrite `makeAdder` into flat, closure-free pseudocode with an explicit environment struct?
Is a closure the same thing as a lambda / anonymous function?
No — a lambda is just syntax for writing an anonymous function expression; a closure is the runtime value you get when that expression (or any function definition with free variables) is evaluated in some environment. You can have named functions that are also closures (any nested named function capturing an outer variable is one), and you can, in principle, have a lambda whose body has no free variables at all, in which case its "closure" carries an empty environment and behaves just like a plain function pointer. The terms are often used loosely as synonyms in casual conversation, but precisely, "lambda" describes syntax and "closure" describes the resulting runtime object.
Do closures capture variables or values?
Variables — specifically, storage locations — not the value present at the moment of capture. This is why a closure sees later mutations to a captured variable, and why two closures created in the same scope that capture the same variable share a single, mutable, common location. If you want a snapshot of a value at closure-creation time, you typically introduce a fresh local binding initialized from the value you want frozen, which the closure then captures as its own distinct location, immune to later mutation of the original.
Why do closures need heap allocation instead of just living on the stack?
Because a closure can be called after the function that created it has already returned, at which point its stack frame has been popped and reused for other calls. If the captured variables lived only in that stack frame, they would be garbage or overwritten by the time the closure runs. So any variable that a closure captures must be allocated somewhere that outlives the creating frame — the heap — and kept alive by the garbage collector (or an equivalent ownership mechanism) for as long as some live closure might still reference it. Compilers use escape analysis to promote only the variables that actually need this treatment, keeping everything else cheaply on the stack.
How is a closure different from an object with fields, in an object-oriented language?
They express the same underlying idea — bundling state with behavior — from opposite directions. An object exposes multiple named methods that all share access to the same fields, with the fields visible (at least to the class) as a structured, inspectable record. A closure exposes exactly one function, whose captured environment is entirely opaque from the outside: there is no way to peek at or enumerate a closure's captured variables from outside its body. In languages with both features, a common pattern is to return several closures that all close over the same shared local variables — this simulates a lightweight object with private state and no formally declared "class" at all.
What is closure conversion, and why do compilers do it?
Closure conversion is a compiler pass that rewrites nested functions with free variables into ordinary top-level functions that take an explicit extra "environment" argument, plus code that builds a concrete data structure (often called an environment record or a display) holding the captured variables at each closure-creation site. It exists because most target machine architectures and calling conventions have no native notion of "a function that also carries data" — everything ultimately compiles down to flat functions and explicit memory, so the abstraction of a closure must be compiled away into that lower-level model. This pass is standard in compilers for OCaml, Haskell (GHC's STG machine), Scheme, and similar functional-language implementations.
Can closures cause memory leaks?
Yes, and it is a genuinely common real-world bug, especially in long-lived JavaScript applications. If a closure captures a reference to some large object (a big array, a DOM node, a socket) but the closure itself is kept alive far longer than intended — for example, stored in a global event-listener table that is never cleaned up — then the large captured object cannot be garbage-collected either, since it is still reachable through the closure's environment. The fix is usually to explicitly remove the closure (unregister the event listener, clear the reference) once it is no longer needed, or to have the closure capture only the small piece of data it truly requires rather than an entire enclosing object.
How does lexical scoping (which closures rely on) differ from dynamic scoping?
Under lexical (static) scoping, a free variable in a function body is resolved according to where the function was defined in the source text — the environment captured at definition time, exactly as closures implement. Under dynamic scoping, a free variable is instead resolved according to the call chain at runtime — whichever binding for that name happens to be innermost among the currently active calls, regardless of where the function was written. Lexical scoping is what almost all modern languages use because it makes a function's meaning predictable from its source code alone, without needing to know who might call it; dynamic scoping (used in early Lisp, and still in some shell/Emacs Lisp constructs) can make refactoring and reasoning about code far harder, since a function's behavior can change based on caller context.