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
});
}
Immediate invocation (IIFE). Before ES modules, a file had no scope of its own: everything declared outside functions went into the global scope. So a module was wrapped in a function that was called right away: an Immediately Invoked Function Expression. Variables inside live in the closure, and only what the function returns gets out:
live example
const counter = (function () {
let count = 0;
return { next: () => ++count };
})();
console.log(counter.next());
console.log(counter.next());
console.log(typeof count);
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Today every module has its own scope, and IIFE shows up in old code and in one common case: (async () => { ... })(), to use await in a CommonJS file where top-level await is not allowed. The pitfall is a line before an IIFE without a semicolon: the parenthesis continues the previous expression and tries to call it as a function.
live example
// error: without a semicolon the parenthesis calls the number 1 as a function
const total = 1
(function () {
console.log("never runs");
})();
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
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.