Scope and closures in JavaScript
A closure isn't a special syntax or a feature you opt into — it's simply what happens whenever a function is defined inside another function, and understanding that it happens ALWAYS (not just when convenient) explains both its power and its most common bug.
3 min read
A closure is just a function that remembers its birth scope
function makeCounter() {
let count = 0; // a variable local to makeCounter's own scope
return function () {
count += 1; // this inner function can still read AND write count, even after makeCounter has returned
return count;
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2 — count genuinely persisted between calls, it wasn't resetWhen makeCounter returns, its own execution is finished — but the inner function it returned keeps a live reference to the variables that were in scope where it was defined, not just a snapshot of their values at that moment. This is a closure: the inner function "closes over" count, and as long as that inner function exists somewhere, count genuinely stays alive in memory, even though makeCounter's own call has long since completed. This isn't a special case — it's simply how JavaScript functions always work; there's no opt-in syntax for it.
Why this makes closures useful for genuine data privacy
function makeAccount(initialBalance) {
let balance = initialBalance; // genuinely INACCESSIBLE from outside — no direct reference exists
return {
deposit: (amount) => { balance += amount; },
getBalance: () => balance,
};
}
const account = makeAccount(100);
account.deposit(50);
account.getBalance(); // 150
account.balance; // undefined — there is no direct way to reach the real variable at allBecause balance only exists inside makeAccount's own closure, and the only way to interact with it is through the functions deliberately returned, there's no way for outside code to read or overwrite balance except through the sanctioned deposit/getBalance interface — a real, enforced form of data privacy, achieved entirely through the closure mechanism, with no special "private" keyword needed (the way some other languages require one).
The classic closure bug: the same variable captured by every iteration
// The stale-closure bug this domain's React section covers too, at its ROOT mechanism
var callbacks = [];
for (var i = 0; i < 3; i++) {
callbacks.push(function () { console.log(i); });
}
callbacks.forEach((cb) => cb()); // prints 3, 3, 3 — every closure captured the SAME iEvery function pushed into callbacks closes over the exact same i — because var has no block scope (covered in the first lesson of this domain), there's only ever one i for the entire loop, and by the time any callback actually runs, the loop has already finished and i is 3. This is the same underlying mechanism behind React's stale-closure bugs (a useEffect callback capturing an old value of state) — different framework, identical root cause: a closure captures a reference to a variable, not a snapshot of its value at closure-creation time, and if that variable's value changes later, every closure over it sees the new value together.
The fix: give each iteration its own variable to close over
let callbacks = [];
for (let i = 0; i < 3; i++) { // let creates a FRESH i per iteration
callbacks.push(function () { console.log(i); });
}
callbacks.forEach((cb) => cb()); // prints 0, 1, 2 — each closure has its OWN iSwitching var to let fixes this because let's block scoping means each loop iteration gets a genuinely distinct i binding — each closure now captures a different variable entirely, rather than all three sharing one. This single fix (var → let) resolving an entire category of closure bug is a large part of why let became the default recommendation over var in modern JavaScript.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What is a closure, mechanically?
2. How do closures enable genuine data privacy, as in the makeAccount example?
3. Why does a `var`-based loop pushing closures into an array have every closure log the same final value?