Closures
Functions that remember their scope, private state, and the loop bug.
A closure is a function bundled with the variables around it at creation. It is the most-asked JavaScript interview topic because it explains private state, callbacks and half of all async bugs in one mechanism.
Real-life situation
Two shopping carts on one page must not share items. A cart factory returns
add and count functions closing over a private items array — each cart
gets its own scope, so adding to one never touches the other. No classes, no
naming tricks; the scope boundary is the privacy boundary.
Seen in: React's useState closes over each render's scope, which is why
late-running callbacks can see stale values (React 16.8, 2019,
useState reference).
Visual
Try it
function createCart() {
const items = [];
return {
add: function (item) {
items.push(item);
},
count: function () {
return items.length;
},
};
}
const first = createCart();
const second = createCart();
first.add("book");
first.add("pen");
console.log(first.count());
console.log(second.count());
function counter() {
let n = 0;
return function () {
n = n + 1;
return n;
};
}
const next = counter();
console.log(next());
console.log(next());Expected output
2 0 1 2
Try adding a third cart and sharing one items array between two factories —
watch independence break. Then convert counter to an object with reset.
Each render in UI code closes over its own props the same way, which is why
stale values surprise even experienced developers.
Loop with var — guess first, then reveal.
for (var i = 0; i < 3; i = i + 1) {
setTimeout(function () {
console.log(i);
}, 0);
}Interview questions
What is a closure?basic
A closure is a function that remembers the variables from where it was created, even after that outer function has finished. The inner function carries a live link to those variables — not a snapshot — so later calls see the latest values. Every callback, handler and private counter in JavaScript leans on this: it is the language's built-in privacy and memory mechanism.
Interviewers also ask
- Does the closure copy the variables or link to them?
How do closures create private state?basic
Put a variable inside a function and return inner functions that use it: outsiders can call those functions but never touch the variable directly. A cart factory returning add and total keeps items hidden — no property to poke, no global to clash. That is real encapsulation with zero syntax: the scope boundary IS the privacy boundary.
Interviewers also ask
- How is this different from a #private class field?
Why does var in a loop break with setTimeout, and what are the fixes?intermediate
var is function-scoped, so all five iterations share ONE i; by the time the timers run, the loop has finished and every callback logs 5. Three fixes: use let (fresh binding per iteration), wrap the body in an IIFE capturing the current value, or pass i as an argument. Interviewers want the mechanism (shared binding vs per-iteration binding), not just the let spell.
Interviewers also ask
- What does each timer log with let, exactly?
Do closures capture values or variables?intermediate
Variables — live links, not snapshots. If the outer variable changes after the closure is created, the closure sees the new value on its next call. This surprises people in loops (all callbacks sharing one var) and in effects that read "stale" state. When you need a snapshot, copy the value into a new variable (or a parameter) at capture time.
Interviewers also ask
- How does this cause stale-state bugs in UI code?
Where do you meet closures in everyday code?basic
Everywhere a function outlives its creation: event handlers reading outer settings, setTimeout callbacks, fetch().then() chains, array mappers using outer totals, and module-level caches. If you have ever written a button handler that remembers which item it belongs to, you have shipped a closure — the interview just wants you to name the mechanism.
Interviewers also ask
- Point to the closure in your last project.
Can closures cause memory leaks?intermediate
Yes, when a long-lived closure holds a big variable nothing else needs — a global cache of callbacks, a forgotten timer, an event listener on a removed element. The garbage collector frees only unreachable memory, and anything the closure can still reach counts as reachable. Fix: clear the timer, remove the listener, or null the reference when the feature is done.
Interviewers also ask
- How would you find which closure holds the memory?
What is the module pattern, in one sentence each for how and why?intermediate
How: wrap code in a function (or module) and expose only chosen functions, keeping the rest in closure scope. Why: it gives private state and a tiny public surface without any class machinery — the pattern behind countless libraries before ES modules, and still the mental model for what a module boundary does.
Interviewers also ask
- What do ES modules add on top of this?
What does a counter function remember between calls?basic
Its closed-over variable. Each call to the outer factory creates a fresh scope, so two counters from the same factory count independently — that independence is the giveaway that closures link scopes, not code. If both counters shared a number, the variable would be outside the factory, and there would be no closure doing the remembering.
Interviewers also ask
- How would you reset one counter without touching the other?
Why do React-style effects sometimes see stale values, in closure terms?intermediate
The effect callback closes over the render's variables — a snapshot of that moment's props and state. If the effect runs later (a timer, a subscription callback), it still reads the old render's values, not the current ones. Fixes all follow the same idea: re-create the closure when values change (dependencies), read latest values through a ref, or clean up the old subscription first.
Interviewers also ask
- What does the dependency list actually do?
Explain debounce in terms of closures.intermediate
Debounce returns a function that closes over a timer id. Each call clears the pending timer and starts a new one, so the real work runs only after calls stop for the wait period. The closed-over timer id is the entire memory of the debouncer — without the closure there would be nowhere private to keep it. Same story for throttle and once.
Interviewers also ask
- What is the difference between debounce and throttle?
Key takeaways
- Closures link live variables, not snapshots.
- Factory + closure is the simplest private state in the language.
- Shared loop bindings (
var) are the classic closure bug;letfixes it.
References
Last verified: 2026-10-12.
Runner helpers
sleep(ms)waits;fakeFetch(url)returns deterministic sample data. Console code runs in a worker with a 5-second limit.