Closures
Functions remember where they were born — the mechanism behind hooks, modules, and half of interview questions.
35 minDifficulty 3/5languageHand-writtenWritten by a person and not yet reviewed by a second one. Checked automatically: schema, the pedagogical rules the hand-written material is held to, and every diagram parsed for real.
Before this
Why this exists
Closures are the interview question everyone dreads and the mechanism everyone uses hourly without noticing. Every React hook, every module with private state, every debounce, every callback that remembers something — all closures. The reason the topic feels slippery is that it is usually taught as a puzzle about `for` loops rather than as what it actually is: a function keeps a live reference to the scope it was created in, and that scope stays alive for exactly as long as the function does.
The mental model
A backpack a function is issued at birth. It does not hold copies of the values it needs — it holds the room they live in. Take the function anywhere, call it years later, and it can still see into that room, including anything that changed after it left. Two functions born in the same room share the room, not a snapshot of it.
How it works
A closure captures the variable, not the value
This is the whole concept, and almost every closure bug is a violation of it. When a function is created it keeps a reference to the scope it was defined in — not a copy of the values in that scope. If something changes that variable later, the function sees the new value, because it is looking at the same storage location. This is why the classic `for (var i = ...)` loop logs the final value three times rather than zero, one, two: all three functions closed over the same `i`, and by the time any of them ran, the loop had finished. Switching to `let` fixes it not by copying but because `let` creates a fresh binding per iteration, so there genuinely are three different variables to capture.
Scope is decided by where code is written, not where it runs
JavaScript uses lexical scoping: which variables a function can see is fixed by its position in the source, permanently, at the moment it is defined. Passing a function into another module, storing it in an array, calling it from a timer years later — none of that changes what it can see. This is the opposite of `this`, which is decided at call time by how the function was invoked, and the collision between those two rules is why arrow functions exist. An arrow function has no `this` of its own, so `this` inside it resolves lexically like any other variable, which is why arrows fixed the callback problem that `var self = this` used to solve by hand.
Closures are how JavaScript does private state
A variable declared inside a function and referenced by a returned function is genuinely inaccessible from outside — not conventionally private like an underscore prefix, but unreachable. No property access, no `Object.keys`, no debugger inspection of the object. This is the module pattern, and it is the reason `useState` can hand you a value and a setter that agree with each other without exposing the storage between them. It is also why every counter, cache, memoiser, rate limiter and event emitter you have written works: they are all a closed-over variable plus functions that can see it.
A live scope cannot be garbage collected
The flip side of keeping the room alive is that the room stays allocated. As long as any closure referencing a scope is reachable, everything in that scope is retained — including large objects the function never actually uses, because engines capture the scope rather than performing perfect per-variable analysis. This is a real source of memory leaks: an event listener registered inside a component and never removed keeps the entire surrounding scope alive, and if that scope held a DOM node or a large buffer, so does the leak. The fix is unremarkable and easy to forget — remove listeners, clear intervals, and cancel subscriptions when the thing that created them goes away.
The stale closure is the React bug you will actually hit
Because a closure sees the variables from the render that created it, a callback stored somewhere long-lived keeps seeing that render's values forever. An effect with an empty dependency array captures the first render's state and never updates. A `setInterval` created once logs the initial count on every tick. This is not React being strange — it is closures working exactly as specified, meeting a library that re-runs your function repeatedly. The three standard escapes are: put the value in the dependency array so a fresh closure is made, use the functional form of the setter so you never read the stale value at all, or keep the value in a ref, which is a stable box whose contents you read at call time rather than capture time.
The mechanism
Two functions, one shared scope: ```js function makeCounter() { let count = 0; // lives in makeCounter's scope return { increment: () => ++count, // captures the SCOPE, not the 0 get: () => count, }; } const a = makeCounter(); const b = makeCounter(); // a separate scope entirely a.increment(); a.increment(); a.get(); // 2 — both functions see the same count b.get(); // 0 — different call, different scope count; // ReferenceError: genuinely unreachable ``` The two calls to `makeCounter` create two independent scopes, which is why `a` and `b` do not interfere. Within one of them, `increment` and `get` share a single `count` — if they had captured *values* rather than the scope, `get` would return `0` forever.
flowchart TD
subgraph outer[makeCounter scope]
C[count = 0]
end
subgraph fns[Returned functions]
I[increment]
G[get]
end
I -->|reads and writes| C
G -->|reads| C
OUT[Outside code] -->|can call| I
OUT -->|can call| G
OUT -.->|cannot reach| C
C -->|kept alive while<br/>either function lives| GC[Not collectable]What people get wrong
- A closure copies the values it needs.
- It holds a live reference to the scope. Later changes are visible. This single misunderstanding produces the var loop puzzle, the stale-closure bug in React, and surprise at two closures over one scope seeing each other's writes. Getting it right makes all three obvious.
- let fixes the loop problem by copying the value each iteration.
- let creates a genuinely new binding per iteration, so there are N distinct variables to close over. The distinction matters because it shows the rule never changed — each closure still captures a variable by reference. What changed is how many variables exist.
- Closures are a special feature you opt into.
- Every function in JavaScript is a closure. It is how the language resolves identifiers. Thinking of it as opt-in makes the behaviour feel arbitrary. Once you accept that every function carries its birth scope, nothing about it is surprising any more.
- Closures leak memory.
- They retain their scope for as long as they are reachable, which is correct behaviour. Leaks come from keeping the closure alive longer than intended. The mechanism is not the bug. An unremoved event listener or uncleared interval is the bug, and the retained scope is the consequence — which is why cleanup functions exist.
- The stale closure in a React effect is a React bug.
- It is closures behaving exactly as specified, inside a function that gets called many times. Each render creates new functions closing over that render's variables. An effect that does not re-run keeps the first render's closure, so it keeps that render's values. Understanding this makes the dependency array obvious instead of mystifying.
When not to use it
- You need many instances each holding substantial state, in a hot path.
- A class. Methods live once on the prototype, whereas a closure-based factory allocates fresh function objects per instance. It rarely matters, and when it does it matters a lot.
- You want private state that debugging tools can still inspect.
- A conventionally-private field, or a WeakMap. Closure state is genuinely unreachable, which is exactly what you want in production and occasionally painful at 2am.
- The captured value must be read at call time rather than capture time.
- A mutable container — a ref, or an object you read a property from. That is precisely what React refs are for.
Terms
- Closure
- — A function together with the scope it was defined in, which it keeps a live reference to.
- Lexical scope
- — Scope determined by where code is written in the source, fixed at definition time and never changed by how the function is called.
- Binding
- — The association between a name and a storage location. let creates a new one per loop iteration; var does not.
- Stale closure
- — A closure holding values from an earlier render or call that no longer reflect current state.
- Module pattern
- — Using a closure to expose functions while keeping their shared state unreachable from outside.
- Temporal dead zone
- — The period between entering a scope and a let or const declaration being evaluated, during which accessing the name throws.
In an interview
What does this log, and why? for (var i = 0; i < 3; i++) setTimeout(() => console.log(i));
- logs 3, 3, 3
- var is function-scoped, so all three callbacks close over one shared i
- the timeouts run after the loop finishes, at which point i is 3
- let fixes it by creating a new binding per iteration, giving three distinct variables
- an IIFE was the pre-let workaround, and it worked by creating a new scope per iteration
How would you implement a function that can only be called once?
- a boolean flag in the enclosing scope, captured by the returned function
- the flag is unreachable from outside, which is what makes it tamper-proof
- cache and return the first result so repeat calls are consistent
- release the reference to the original function afterwards so its scope can be collected
Can you recall it?
Explain what a closure captures, and use that to explain both the classic `var` loop result and the stale-closure bug in a React effect.