Closures show up in nearly every JavaScript codebase, often without anyone naming them explicitly — understanding the mechanism directly clears up a category of bugs that otherwise look mysterious.
The core idea
A closure is a function bundled together with references to the variables from the scope it was created in — even after that outer scope has finished running:
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3makeCounter() runs and returns, but the inner function still has access to count — it's not re-created or reset on each call, because the returned function closed over that specific count variable, not a copy of its value at creation time.
Each call creates a genuinely separate closure
const counterA = makeCounter();
const counterB = makeCounter();
counterA(); // 1
counterA(); // 2
counterB(); // 1 — its own separate `count`, unaffected by counterAEvery call to makeCounter() creates a fresh count variable and a fresh closure over it — counterA and counterB don't share state, even though they were created from the same function.
The classic loop bug closures explain
// The bug
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// logs: 3, 3, 3 — not 0, 1, 2var is function-scoped, not block-scoped — there's only one i shared across all three loop iterations, and by the time the setTimeout callbacks actually run, the loop has finished and i is 3. Every closure captured the same variable, not a snapshot of its value at each iteration.
// The fix
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// logs: 0, 1, 2 — correctlet is block-scoped — each loop iteration gets its own fresh i, so each closure captures a genuinely distinct variable. This single distinction between var and let is the most common real-world encounter developers have with closures, usually while debugging exactly this symptom.
A practical use: private state without a class
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) { balance += amount; return balance; },
withdraw(amount) {
if (amount > balance) throw new Error("Insufficient funds");
balance -= amount;
return balance;
},
getBalance() { return balance; },
};
}
const account = createBankAccount(100);
account.deposit(50); // 150
account.withdraw(30); // 120
// balance itself is not accessible from outside — only through the methodsbalance is genuinely private — there's no way to read or modify it except through the returned methods, because it only exists inside the closure createBankAccount created. This "module pattern" predates JavaScript classes and is still a legitimate way to get real encapsulation without a class at all.
Closures and React
Anyone who's written a React component has used closures constantly, often without noticing:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1); // closes over `count` from this specific render
}
return <button onClick={handleClick}>{count}</button>;
}handleClick closes over the count value from the render it was created in — this is exactly the mechanism behind React's "stale closure" bugs, where an event handler or effect callback references an outdated value because it captured an earlier render's variables, not the current one.
Closures and currying
Closures are also what makes currying — transforming a function that takes multiple arguments into a chain of functions each taking one — possible at all:
function multiply(a) {
return function (b) {
return a * b;
};
}
const double = multiply(2);
double(5); // 10
double(10); // 20
multiply(3)(4); // 12 — called directly, without an intermediate namemultiply(2) returns a closure that remembers a = 2 — every subsequent call to that returned function multiplies whatever's passed by the remembered a. This is the exact same mechanism as makeCounter above, applied to build a reusable, partially-configured function instead of stateful counters.
Debugging a closure with the browser's dev tools
When a closure holds a value you need to inspect, Chrome DevTools' scope inspector (visible when paused at a breakpoint inside the inner function) shows a "Closure" section listing exactly which outer variables the current function has captured — a genuinely useful way to confirm what a specific closure actually has access to, rather than reasoning about it purely from reading the source.
Closures in a memoization cache
A practical, common use of closures: caching expensive computation results without polluting the outer scope with a separate cache variable:
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}
const slowSquare = (n) => { /* expensive work */ return n * n; };
const fastSquare = memoize(slowSquare);cache is only accessible to the returned function — nothing outside memoize can read or corrupt it directly, which is exactly the same private-state pattern as the bank account example above, applied to a genuinely common real-world need.
Common mistakes
- Using
varin a loop that creates closures (event handlers, timers, async callbacks) and being surprised every closure sees the same final value. Switch tolet, or manually create a new scope per iteration with an IIFE if you're stuck withvarfor some reason. - Assuming a closure captures a variable's value at creation time. It captures a reference to the variable itself — if the outer variable changes later, every closure over it sees the new value, not a frozen snapshot.
- Creating closures in a hot loop unnecessarily (a new function defined inside a frequently-called function, capturing the same unchanging outer values every time) — usually harmless, but worth hoisting the function out if it doesn't actually need to be recreated each call.
- Not cleaning up closures that hold references to DOM nodes or large data structures in long-lived event listeners — the closure keeps everything it references alive, which is correct behavior but a real memory cost if the listener is never removed.
Related reading
- The JavaScript Event Loop, Explained With Diagrams — shares tags: javascript, programming (same category).
- Modern Array Methods You Should Be Using Instead of Loops — shares tags: javascript, programming.
- React useEffect Cleanup: A Practical Guide — shares tags: javascript.
- TypeScript Generics Explained: A Practical Introduction — shares tags: javascript, programming.
- How to Migrate a JavaScript Codebase to TypeScript Incrementally — shares tags: javascript, programming.