JavaScript closures guide

How Closures Work in JavaScript

Understand how a function keeps a live connection to the environment it was created in, with runnable examples covering state, loops, and private data.

A closure is not a copy of values from an outer function. It is the fact that a function keeps a live reference to the lexical environment it was created in, so it can still read and update that environment’s bindings long after the surrounding call has returned.

This guide builds from a single persistent counter to the classic per-iteration loop mistake, then to using a closure to keep data genuinely private.

The short answer

  • A closure is fixed at creation time: whatever lexical environment a function is written inside becomes the environment it keeps forever, no matter where it is later called from.
  • Every call to a closure resolves its free variables through that same captured environment, which is why state keeps accumulating instead of resetting.
  • Each separate call that creates a new function gets its own separate captured environment, even when the two calls run the exact same code.

1. A returned function keeps its outer variables alive

When makeCounter returns increment, the surrounding call has technically finished — but increment still holds a live reference to makeCounter’s environment, so count is never reclaimed and never resets.

increment keeps access to count after makeCounter has already returned

function makeCounter() {
  let count = 0;

  return function increment() {
    count += 1;
    return count;
  };
}

const next = makeCounter();
console.log(next()); // 1
console.log(next()); // 2

2. Two calls create two independent closures

Calling makeCounter a second time creates a brand-new environment with its own count binding. The two returned functions are not linked in any way — incrementing one never affects the other.

counterA and counterB each close over their own count

const counterA = makeCounter();
const counterB = makeCounter();

console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1 — its own, separate count

3. The classic var-in-a-loop mistake

var is function-scoped, not block-scoped, so all three callbacks below close over the exact same i. By the time any of them runs, the loop has already finished and i is 3.

Switching var to let fixes this: let creates a fresh binding for i on every iteration, so each callback closes over its own private value instead, logging 0, 1, 2.

All three timeouts log 3, because they share one i

for (var i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 0);
}
// 3
// 3
// 3

4. Using a closure to keep data private

balance here is never exposed directly — the only way to read or change it is through the functions makeAccount returns, because those are the only functions that close over its environment.

balance is only reachable through the closures that capture it

function makeAccount(initialBalance) {
  let balance = initialBalance;

  return {
    deposit(amount) {
      balance += amount;
      return balance;
    },
    getBalance() {
      return balance;
    },
  };
}

const account = makeAccount(100);
console.log(account.deposit(50)); // 150
console.log(account.getBalance()); // 150

5. When a closure also involves this

A closure captures variables, not this — this is still resolved separately, at call time, for whichever function is actually invoked. The one exception is an arrow function, which closes over this exactly like it closes over any other variable.

See the this & Closures tracer for worked examples that combine both, and the this guide for the call-site rules that decide this on its own.

Try it yourselfTrace a closure capturing stateStep through a call and watch which lexical environment a closure keeps alive after the surrounding function returns.