Closures and lexical scoping
A closure is a function that retains access to variables from the scope it was defined in, even after that outer scope has returned. Lexical scoping means scope is determined by where code is written, not where it's called. Powers module patterns, factories, currying, React hooks' captured values, and event handler state.
Lexical scoping = a name resolves based on where it's written, not where it's called. Closure = a function carries references to the variables of the scope where it was created, so it can read/write them long after that scope has returned.
The canonical example
function counter() {
let n = 0;
return {
inc: () => ++n,
get: () => n,
};
}
const c = counter();
c.inc(); c.inc(); // n is private but persists
c.get(); // 2n is gone from the call stack the moment counter() returns, but inc and get keep a reference to it via the closure. The engine keeps the variable alive as long as anything reachable references it.
Why "lexical"
function outer() {
const x = 1;
function inner() { return x; }
return inner;
}
function caller() {
const x = 99;
return outer()();
}
caller(); // 1 — inner sees the x from where it was *defined*, not *called*Contrast with dynamic scoping (used by some older languages and bash), which would return 99.
Closure use cases
- Data privacy / module pattern — variables captured but not exposed.
- Factories —
makeAdder(5)(3)returns 8. - Currying / partial application — see [[currying-and-partial-application]].
- Callbacks that need context —
setTimeout(() => doThing(user), 1000). - React hooks — every render creates new closures over the latest props/state.
Classic gotcha — var in a loop
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // prints 3, 3, 3
}
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // prints 0, 1, 2 — let creates a per-iteration binding
}var is function-scoped: one shared i. let is block-scoped: a fresh binding per iteration that each closure captures.
Stale closures in React
function Search() {
const [q, setQ] = useState("");
useEffect(() => {
const id = setInterval(() => console.log(q), 1000);
return () => clearInterval(id);
}, []); // empty deps — closes over initial q ("") forever
}Either include q in deps (effect re-runs) or use a ref (useRef) to always read the latest value.
Memory note
Closures keep their captured variables alive. A long-lived handler closing over a huge array → that array can't be GC'd. Be aware in event listeners and subscriptions; null out the variable or attach a smaller derived value.
Interview framing
"Lexical scoping means a name's binding is decided by where the code is written. A closure is a function plus its enclosing scope's variables, kept alive because the function references them. Classic use cases: data privacy, factories, currying, callbacks. The classic gotcha is var in a loop — let fixes it because it creates a per-iteration binding. In React, stale closures from missing useEffect deps are the modern variant; either depend on the value or read through a ref."
Follow-up questions
- •Walk me through the var-in-a-loop bug.
- •How do React hooks rely on closures?
- •When can closures cause memory leaks?
Common mistakes
- •Using var in loops and being surprised by shared bindings.
- •Missing useEffect dependencies — stale closures.
- •Holding large objects in long-lived closures.
Performance considerations
- •Closures aren't expensive on their own, but they pin captured variables in memory. Detach large objects after use.
Edge cases
- •Hoisting interactions with var.
- •Closures over loop counters before block scope existed.
- •this is NOT lexical — arrow functions capture this lexically, regular functions don't.
Real-world examples
- •Module pattern in pre-ESM code, React hooks, event handlers, currying utilities.