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 for loop + setTimeout printed 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

  1. Reproduce the var + setTimeout bug, then fix with let.
  2. Write makeCounter and create two independent counters.
  3. 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.