JavaScriptIntermediate

JavaScript Closures Explained: How Functions Remember

What a closure really is, how a JavaScript function keeps its variables after the outer function returns, and the patterns and bugs closures cause.

A counter function inside the finished makeCounter scope, still pointing at the variable count
On this page

Closures sound like an advanced topic, but you have almost certainly used one already. Every event handler that reads a variable from outside, every setTimeout callback, every React hook leans on them.

In this article you'll learn what a closure actually is, why a function can still use variables after the function that created them has returned, the patterns closures make possible, and the two bugs they cause most often.

Start with scope#

In JavaScript, where a variable can be used is decided by where it is written in the code. This is called lexical scope. An inner function can see the variables of every function it sits inside:

JavaScript
function outer() {
  const message = "Hello from outer";

  function inner() {
    console.log(message); // inner can see outer's variables
  }

  inner();
}

outer(); // "Hello from outer"

Nothing surprising yet: inner runs while outer is still running, so of course message exists. The interesting question is what happens when the inner function outlives the outer one.

A function that remembers#

Here makeCounter creates a variable, returns an inner function, and then finishes:

JavaScript
function makeCounter() {
  let count = 0;

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

const counter = makeCounter();

counter(); // 1
counter(); // 2
counter(); // 3

By the time we call counter(), makeCounter has already returned. Its local variable count should be gone, yet the counter keeps reading and updating it.

That's a closure: a function bundled together with the variables from the scope where it was created. When JavaScript creates the inner function, it gives it a hidden reference to its surrounding scope. As long as the function exists, that scope stays alive.

const counter = makeCounter(); makeCounter()has already returned its variablescount: 3still alive countercount += 1 createdrefers to The closure: counter keeps a reference to the scope it was created in, so it can still read and update count
makeCounter has returned, but the counter function still holds a reference to its variables, so count survives

Every function is a closure

Technically every JavaScript function keeps a reference to the scope it was created in. We usually only say "closure" when that matters, when a function uses outer variables after the outer function has finished.

Closures capture variables, not values#

A closure doesn't take a snapshot. It keeps a live link to the variable itself, so it always sees the current value:

JavaScript
let greeting = "Hello";

function greet(name) {
  return `${greeting}, ${name}!`;
}

greeting = "Salam";
greet("Sara"); // "Salam, Sara!"

This is also why calling makeCounter() twice gives you two independent counters. Each call runs the function body again and creates a new count variable, and each returned function closes over its own:

JavaScript
const a = makeCounter();
const b = makeCounter();

a(); // 1
a(); // 2
b(); // 1  (b has its own count)

Pattern 1: private state#

JavaScript code outside makeCounter has no way to reach count directly. Only the functions created inside can touch it. That makes closures a simple way to hide data:

JavaScript
function createAccount(initialBalance = 0) {
  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 = createAccount(100);
account.deposit(50);   // 150
account.withdraw(30);  // 120
account.balance;       // undefined: the variable is not a property

The only way to change balance is through deposit and withdraw, so the rule "you can't withdraw more than you have" can't be bypassed. Modern classes offer #private fields for the same job, but the closure version works everywhere and needs no this.

Pattern 2: function factories#

A function that returns functions can "pre-configure" them. Each returned function remembers the arguments it was made with:

JavaScript
function multiplier(factor) {
  return (n) => n * factor;
}

const double = multiplier(2);
const triple = multiplier(3);

double(5); // 10
triple(5); // 15

The same idea gives you small utilities you'd otherwise need a library for. once runs a function a single time and remembers the result:

JavaScript
function once(fn) {
  let called = false;
  let result;

  return function (...args) {
    if (!called) {
      called = true;
      result = fn.apply(this, args);
    }
    return result;
  };
}

const init = once(() => console.log("Connecting…"));
init(); // "Connecting…"
init(); // nothing: already done

And memoize caches results in a Map that only the returned function can see:

JavaScript
function memoize(fn) {
  const cache = new Map();

  return (arg) => {
    if (!cache.has(arg)) cache.set(arg, fn(arg));
    return cache.get(arg);
  };
}

const slowSquare = (n) => { /* imagine heavy work */ return n * n; };
const fastSquare = memoize(slowSquare);
fastSquare(9); // computed
fastSquare(9); // from the cache

Pattern 3: event handlers and callbacks#

Callbacks run later, long after the code that registered them has finished. Closures are how they still know what to work on:

JavaScript
function setupLikeButton(button) {
  let likes = 0;

  button.addEventListener("click", () => {
    likes += 1;
    button.textContent = `Likes: ${likes}`;
  });
}

document.querySelectorAll(".like").forEach(setupLikeButton);

Every button gets its own likes variable, with no global state and no data- attributes to keep in sync.

Bug 1: the classic loop with var#

What does this print?

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

Not 0, 1, 2. A var declaration is scoped to the whole function, so there is only one i, shared by all three callbacks. By the time they run, the loop has finished and i is 3.

The fix is one word. With let, the loop creates a new binding of i for every iteration, and each callback closes over its own:

JavaScript
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 0, 1, 2

Prefer let and const

Bugs like this are the main reason modern JavaScript avoids var. Use const by default and let when a variable must change.

Bug 2: stale closures#

The flip side of "each call creates new variables" is that a function created during one call keeps seeing that call's variables, even when newer ones exist. React developers meet this all the time:

Jsx
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setSeconds(seconds + 1); // always reads seconds from the first render: 0
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <p>{seconds}</p>;
}

The timer shows 1 and then stops changing. Every render of Timer is a new function call with its own seconds constant, but the interval callback was created during the first render, so it keeps reading 0 forever and setting the value to 0 + 1.

The fix is to stop depending on the captured value. Pass an update function instead, and React hands you the current value each time:

JavaScript
setSeconds((current) => current + 1);

Closures and memory#

A closure keeps alive everything it uses. Usually that's exactly what you want, but it can hold on to more memory than you expect:

JavaScript
function attachReport(element) {
  const rows = loadHugeReport(); // say, 50 MB of data

  element.addEventListener("click", () => {
    console.log(`${rows.length} rows`);
  });
}

As long as that listener is attached, rows can never be freed, even though the handler only needs a number. Engines such as V8 only keep the variables an inner function actually uses, so capture the small thing instead of the big one:

JavaScript
function attachReport(element) {
  const rowCount = loadHugeReport().length;

  element.addEventListener("click", () => {
    console.log(`${rowCount} rows`);
  });
}

And when a handler is no longer needed, remove it (removeEventListener, or the signal option with an AbortController) so its closure can be garbage-collected.

Recap#

  • Lexical scope: a function can use the variables of the code it's written inside.
  • A closure is a function plus a live reference to the scope it was created in. That scope survives as long as the function does.
  • Closures capture variables, not values, and every call of the outer function creates fresh variables.
  • Use them for private state, function factories like once and memoize, and callbacks.
  • Watch out for var in loops, stale closures that read old values, and closures that keep large objects alive.

Once you see functions as "code plus the variables it remembers", a lot of JavaScript that looked like magic starts to look obvious.