← back to the section

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.
  • 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.