What you'll be able to do
By the end of this page you should be able to explain a closure as a function that still sees variables from the place it was created, fix the loop + setTimeout bug, and use closures on purpose in UI code (counters, private state).
Previous post: composition vs inheritance.
Who this is for
- People whose
forloop +setTimeoutprinted the same number ten times - Beginners wiring click handlers that all share one
let i - Anyone who heard "closure" in an interview and only got a textbook definition
You can skip this if loop + listener patterns already make sense. Closures show up again with async and React effects.
Closure in plain words
A function "closes over" variables from its outer scope — even after the outer function finished.
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const next = makeCounter();
next(); // 1
next(); // 2
Scenario A — private counter. Nothing outside can set count directly; only next can change it.
Scenario B — factory. Each call to makeCounter() gets its own count — two counters do not share state.
The rule: inner function + referenced outer variables = closure.
Real UI: one handler, remembered value
function setupGreet(name) {
return function () {
console.log("Hello, " + name);
};
}
button.addEventListener("click", setupGreet("Asha"));
Scenario A — config captured once. The handler remembers name without storing it on window.
Scenario B — many buttons in a loop. If you use var i, every handler might share the same final i; let per iteration or an IIFE / forEach fixes it (below).
Loop + setTimeout — the classic bug
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}
// Often prints 3, 3, 3
Scenario A — var hoisting. One shared i; when timers run, the loop is done and i is 3.
Scenario B — fix with let. for (let i = 0; ...) creates a fresh i per iteration — each timeout closes over its own value.
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}
// 0, 1, 2
Another fix: pass i into a factory (function (n) { ... })(i) — explicit closure per value.
A tiny practice file
<!DOCTYPE html>
<html>
<body>
<div id="buttons"></div>
<pre id="out"></pre>
<script>
const wrap = document.getElementById("buttons");
const out = document.getElementById("out");
for (let i = 0; i < 3; i++) {
const btn = document.createElement("button");
btn.textContent = "Btn " + i;
btn.addEventListener("click", function () {
out.textContent = "clicked " + i;
});
wrap.appendChild(btn);
}
setTimeout(function () {
out.textContent += "\nTimers: ";
for (let j = 0; j < 3; j++) {
setTimeout(function () {
out.textContent += j + " ";
}, 50 * j);
}
}, 0);
</script>
</body>
</html>
Each button should log its own index; timers should print 0 1 2 — not three 3s.
Mistakes I see a lot
1. Using var in loops that create async or event callbacks. Switch to let or a factory parameter.
2. Creating closures in hot paths without noticing memory. Holding DOM nodes in closures can keep elements alive — rare in small apps, worth knowing.
3. Thinking closures are only a trick. They are how modules, hooks, and debouncers remember state.
4. Fixing loop bugs with bind when let is enough. Use the simplest fix first.
What to try before the next post
- Reproduce the
var+setTimeoutbug, then fix withlet. - Write
makeCounterand create two independent counters. - Build the tiny HTML file.
Next in this series: execution context and call stack — how the engine runs your functions in order.
Try this next outside the series
These mental models become more useful when you can point at a real framework problem they explain.
- React Server Components explained — see execution boundaries and composition decisions in a modern app
- Node.js error handling — watch call stacks and thrown errors matter in real request code