The closure is JavaScript's most famous "hard" topic, even though the definition fits in one sentence: a function keeps access to the scope it was created in — even when it runs somewhere else entirely. If you understand lexical scope, you already understand closures — all that's left is to see the consequences.
A function takes its scope along
function makeCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = makeCounter(); // makeCounter finished and "died"...
counter(); // 1
counter(); // 2 — ...but count is alive!
By naive logic, count should vanish when makeCounter completes. But the returned function references it — and the engine keeps the variable alive as long as the function lives. That's what a closure is: not a copy of the value, but a live reference to the variable.
Two independent counters — two independent closures:
const a = makeCounter();
const b = makeCounter();
a(); a(); // 2
b(); // 1 — each call to makeCounter has its own count
Why: private state and factories
Privacy. Before #fields appeared in classes, a closure was the only way to hide data: there is no way to reach count from the outside — no name, no reference. Only through the functions that are "allowed".
function createWallet(initial) {
let balance = initial;
return {
deposit: (n) => (balance += n),
getBalance: () => balance,
};
}
const w = createWallet(100);
w.balance; // undefined — doesn't exist from the outside
w.getBalance(); // 100
Function factories. A function configured with parameters once:
const multiplyBy = (factor) => (n) => n * factor;
const double = multiplyBy(2);
const triple = multiplyBy(3);
double(5); // 10
triple(5); // 15
Each produced function closed over its own factor. This pattern is everywhere: handlers with parameters, debounce/throttle, currying, middleware.
Callbacks with context. Any event handler or timer that uses outer variables is a closure:
function setupSearch(input) {
let lastQuery = "";
input.addEventListener("input", () => {
lastQuery = input.value; // the handler closed over both input and lastQuery
});
}
The trap: the stale closure
The flip side of the "live reference": a closure sees the variable as it is now, not as it was when the function was created. And conversely — if the variable was recreated, an old closure holds the old one. The second case is the famous stale closure:
function startTimer(getValue) {
setInterval(() => {
console.log(getValue()); // always current — the function reads it fresh
}, 1000);
}
// But this way it freezes:
function startTimerBad(value) {
setInterval(() => {
console.log(value); // forever the value it was called with
}, 1000);
}
In React this is trap number one in useEffect: the effect closed over the first render's state and "doesn't see" updates — cured by the dependency array or a functional update. A detailed breakdown is in the article on hooks. The classic with var in a loop from the previous article is also a stale closure: three callbacks holding one variable.
One more practical nuance — memory: a closure prevents the garbage collector from collecting everything it references. A handler that closed over a huge array "just in case" keeps it in memory as long as it lives itself. Removing handlers (removeEventListener, clearInterval) frees their closures too.
Test yourself
let x = 10;
function outer() {
let x = 20;
return () => x;
}
const f = outer();
x = 30;
f(); // ?
Answer: 20. The arrow function closed over the x from outer (the lexically nearest one); the global x = 30 doesn't affect it. The key to any such puzzle: find where the function is written, and walk outward from there.
In short
- A closure = a function + live references to the variables of the scope it was created in. Not a copy — a reference.
- Every call of the outer function creates a new independent closure.
- Uses: private state, function factories (
multiplyBy), handlers and callbacks with context, debounce/throttle. - The stale closure trap: a function holds a variable of its own "generation". In React — the main cause of useEffect weirdness.
- Closures hold on to memory: a removed handler frees everything it closed over.
What to read next
- Scope — the foundation without which closures look like magic.
- Hooks: custom hooks and useEffect pitfalls — the stale closure under real React combat conditions.
- Prototypes — the next topic in the series: the language's second reuse mechanism.