What are closures in JavaScript?
A closure is a function bundled with the variables in scope at the time it was created — it remembers and can mutate those variables long after the outer function has returned.
A closure is the combination of a function with the lexical environment in which it was declared. When that inner function is invoked later — possibly long after the outer function has returned — it still has access to the variables from its surrounding scope. Closures are one of JavaScript's most powerful primitives, and they're the mechanism behind hooks, modules, currying, event handlers, and most "remember this state across calls" patterns in the language.
Why closures exist: lexical scoping. JavaScript resolves a function's free variables (variables it uses but doesn't declare) against the scope in which the function was defined, not the scope in which it is called. This is called static or lexical scoping, and it's the opposite of dynamic scoping (which a few older languages like classic Lisp had). Because the engine has to keep that defining scope reachable for as long as the function might still run, it keeps the entire variable environment alive — that's the "closure."
The mental model. When the JS engine creates a function, it stores a hidden reference to its environment record — the variable bindings in scope at creation. When the function executes, lookups go: own arguments → environment record → enclosing environment records, recursively, until the global scope. This is the scope chain. The closure is literally "function + chain of environment records pinned alive by reachability."
What closures enable:
- Data privacy — the module pattern:
const counter = (() => { let n = 0; return { inc: () => ++n, get: () => n }; })();—nis unreachable from outside; only the returned functions can touch it. Pre-ES6 classes, this was the way to get private state. - Factories and partial application —
function adder(x) { return y => x + y; }—adder(5)returns a new function that hasxpermanently bound to5. - Currying —
mul(2)(3)(4)decomposes a multi-arg function into a chain of single-arg ones, each closing over the previous result. - Callbacks and event handlers — every
element.addEventListener('click', () => doThing(id))is a closure over the surroundingid. - React hooks —
useStatereturns a setter that closes over a fiber-local cell; the entire hooks system is closures over the current fiber. - Memoization — a cache map declared in the outer function persists across calls of the inner.
Classic gotcha: var in a loop.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Logs: 3, 3, 3 — all three callbacks closed over the same i.var is function-scoped, so there's one i shared by all three callbacks. By the time they run, the loop has finished and i === 3. Switching to let fixes it: let is block-scoped, so each iteration gets its own i binding, and each callback closes over its own.
The cost of closures: memory. Closures keep their entire environment record alive — including variables the inner function doesn't actually use, because the engine usually can't prove what's unused. If the inner function survives (stored in a long-lived data structure, an event listener that's never removed, a setInterval callback), the outer scope's variables can't be garbage collected. This is the most common shape of SPA memory leaks: a handler attached on mount and never removed, holding a megabyte of component-local state alive forever. Mitigations:
- Always pair
addEventListenerwithremoveEventListener(or useAbortController). - For
setInterval/setTimeout, store the timer id and clear on cleanup. - Don't capture large objects in long-lived closures when a small id will do.
this is not part of the closure. Common confusion: this is determined by how the function is called (call site), not by where it was defined — except for arrow functions, which inherit this from their enclosing scope at definition time. So an arrow function "closes over" this lexically; a regular function doesn't.
Performance. Closure creation in modern engines is cheap (one allocation for the environment record). Property access is closures' real cost: looking up a variable through the scope chain is slower than reading a known local. Inside hot loops, hoist references to locals.
Interview-ready definition. "A closure is a function plus the lexical scope it was declared in — when the function runs later, it still has access to those variables. It's how JavaScript implements private state, factories, currying, and React's hook model. The trade-off is that it keeps the captured scope alive, which is the most common SPA memory-leak shape."
Code
Follow-up questions
- •How do closures cause memory leaks, and how do you fix them?
- •Implement a once() that lets a function run only on its first call.
- •How do React hooks rely on closures? What's the stale-closure problem?
Common mistakes
- •Confusing closure with the function itself — the closure is function + environment.
- •Reading a value captured by a closure and assuming it reflects current state (stale closure inside useEffect / setInterval).
- •Using var in a loop and expecting each iteration to capture its own value.
Performance considerations
- •Closures hold references to outer scope. A long-lived event listener can pin large objects in memory — null them out on cleanup.
- •Recreating closures inside hot render paths (e.g. inline handlers) is fine in React; the cost is GC pressure, not correctness.
Edge cases
- •Closures over `let`/`const` create per-iteration bindings; over `var` they share one binding.
- •A closure capturing `this` from an arrow function follows lexical `this`; from a regular function it doesn't.
Real-world examples
- •React hooks: each render's callbacks close over that render's state — the source of stale-closure bugs in setInterval.
- •Module pattern (pre-ESM): an IIFE returns an object whose methods close over private state.