Writing · MARGIN NOTESFront-End Development
What Interviewers Mean When They Ask About Closures
Closures are not magic. A closure is a function bundled with its lexical environment, and that environment can outlive the call that created it. Here is the mental model interviewers want, plus the gotchas that trip people up.
- Author
- Nicola Riker Urdaneta
- Published
- September 8, 2026
- Reading time
- 3 min read

Contents
In interviews, "explain closures" is rarely a vocabulary check. Interviewers want to know whether you understand how JavaScript resolves variables, and whether you can predict what a function still sees after its outer function has returned.
A closure is a function bundled with the lexical environment where it was defined. In plain terms: the function keeps access to the variables from the scopes that surround it, even after those outer functions have finished running.
That is it. The rest is consequences.
Mental model: lexical scope first
When JavaScript evaluates an identifier, it looks it up by walking outward through nested scopes. Lexical scope means which scope chain that walk uses is fixed by where the function was defined, not by where it is called.
function outer() {
const label = "outer";
function inner() {
return label;
}
return inner;
}
const fn = outer();
console.log(fn()); // "outer"outer has already returned when fn() runs. The binding label still resolves, because inner closed over the environment where label lived.
Interviewers often rephrase this as: "Does the inner function remember its birthplace?" Yes. The retained environment plus the function is the closure.
Minimal demo: private state
Closures are how you get encapsulation without classes.
function makeCounter() {
let count = 0;
return {
next() {
count += 1;
return count;
},
peek() {
return count;
},
};
}
const a = makeCounter();
const b = makeCounter();
a.next(); // 1
a.next(); // 2
b.next(); // 1
a.peek(); // 2Each call to makeCounter creates a fresh environment. a and b do not share count. That is why module patterns and factory functions feel natural in JavaScript: the closed-over bindings are the private fields.
Gotcha: the loop with var
This classic tripwire still shows up in interviews.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// prints 3, 3, 3All three callbacks close over the same i binding. By the time the timers fire, that binding holds 3.
With let, each iteration gets its own binding:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// prints 0, 1, 2If you must stick with var, wrap each iteration in a function that captures the current value:
for (var i = 0; i < 3; i++) {
((captured) => {
setTimeout(() => console.log(captured), 0);
})(i);
}The interview win is naming the shared binding, not memorizing the snippet.
Gotcha: stale closures in callbacks
Closures capture bindings, not snapshots of values (unless you copy the value into a new binding). The same rule applies when these callbacks run from timers or UI handlers. Side by side:
(a) closes over a copied parameter once and stays stale. (b) closes over the outer binding and re-reads it when the callback runs.
// (a) Stale: each call closes over its own `value` parameter
function makeHandler(value) {
return () => console.log("stale path:", value);
}
let current = 1;
const handler = makeHandler(current); // value holds 1
current = 2;
handler(); // "stale path: 1"
// (b) Fresh: the callback re-reads the outer binding each time
function makeLiveHandler() {
return () => console.log("live path:", current);
}
current = 1;
const live = makeLiveHandler();
current = 2;
live(); // "live path: 2"In (a), makeHandler(current) creates an environment whose value binding was set once. Later mutations to current do not change that captured binding. In (b), the callback closes over current itself and looks it up when it runs. The interview win is naming which binding was closed over, and when that environment was created.
What to say out loud in an interview
A strong answer sounds like this:
- Closures come from lexical scope. Nested functions see outer bindings.
- Those bindings can outlive the outer function call.
- That is useful for private state, factories, and callbacks.
- The common bugs are shared mutable bindings (the var loop) and stale captures when a callback closed over a per-call binding that no longer tracks updates.
Then show one of the demos above. Concrete beats abstract every time.
Takeaways
- A closure is a function plus its lexical environment.
- Closures are how JavaScript keeps "private" state without classes.
- Shared bindings explain the classic loop bug; per-iteration bindings fix it.
- When async code looks wrong, ask which environment the callback closed over.
If you can explain those four points with a small code sample, you are interview-ready on closures.