Closures in JavaScript
What a closure is, why the variable outlives the function that created it, and the loop bug every JavaScript developer hits once.
Updated
What a closure is
A closure is a function that keeps access to the variables around it at the place it was written, and keeps that access after the surrounding function has already returned.
That second half is the whole idea. A function remembering its own arguments is unremarkable. A function remembering variables belonging to a call that finished ten minutes ago is what makes closures worth a name.
function makeCounter() {
let count = 0; // local to this call of makeCounter
return () => ++count; // ...and still reachable from here
}
const next = makeCounter();
next(); // 1
next(); // 2makeCounter returned long ago. Its count did not disappear, because the returned arrow function still refers to it, and a variable that something still refers to cannot be collected. count is now reachable through exactly one door — calling next — and through no other. Nothing outside can read it, assign to it, or even name it.
Why this is the closest thing JavaScript has to a private field
Before #private class fields, this was *the* way to make state that callers cannot corrupt:
function createAccount(initial) {
let balance = initial;
return {
deposit: (n) => { balance += n; },
getBalance: () => balance,
};
}
const acc = createAccount(100);
acc.deposit(50);
acc.getBalance(); // 150
acc.balance; // undefined — there is no such propertyBoth methods close over the same balance. That is the part people get wrong when they first meet closures: a closure does not copy the variable, it keeps a reference to the binding. Two functions created in the same scope share one variable, not two snapshots of it.
The loop bug
This is the closure question asked in interviews, and the reason let was given per-iteration scoping:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 3, 3, 3There is only one i. var scopes it to the whole function, all three callbacks close over that single binding, and by the time the timers fire the loop has finished and left it at 3.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 0, 1, 2let creates a new binding per iteration, so each callback closes over its own. The fix is not that let is "safer" in some vague way — it is that the number of variables changed from one to three.
Where closures cost you
A closure keeps its whole scope alive, not just the parts it uses. The engine is allowed to optimise away what is provably unreachable, but you should not plan around that:
function attach(element) {
const huge = new Array(1e6).fill('...'); // never used below
element.onclick = () => console.log('clicked');
}The handler closes over attach's scope. As long as the element lives, that scope may live, and huge with it. This is a common shape of leak in long-lived pages: a listener that is never removed, holding a scope that holds a large object.
The rule that avoids it is simple — create the closure in the smallest scope that contains what it actually needs.
How to recognise one in the wild
Once you see the pattern, closures turn out to be most of the utilities you already use:
| Utility | What the closure holds |
|---|---|
debounce | the pending timer id |
throttle | the timestamp of the last call |
memoize | the cache |
once | whether it has run, and the first result |
| a curried function | the arguments received so far |
None of these need a class. Each is a function that returns a function, over a variable the caller can never touch.
Now practice it
Reading this page is the cheap half. These are the exercises that make you use it.
- Challengeeasy
Once Wrapper
The smallest real closure there is: one boolean and one cached result that must survive every later call.
- ChallengemediumPro
Curry
Forces you to accumulate arguments across calls — the closure is the only place they can live.
- Quizmedium
Fundamentals · Functions & Scope
Scope and function questions, including the loop binding this page ends on.