Closures

A closure is a function that remembers the variables from the place where it was created, even after that place has finished running. Closures sound abstract, but you use them constantly — in event handlers, timers, callbacks and any function that returns another function.

A first closure

JS counter.js
function makeCounter() {
  let count = 0;
  return function () {
    count++;
    return count;
  };
}

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

makeCounter finished running on line 8, yet the function it returned still reads and updates count. The inner function "closes over" the variable — it keeps a live link to it, not a copy of its value.

Each call creates a separate closure

JS separate.js
function makeCounter() {
  let count = 0;
  return () => ++count;
}

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

a();
a();
console.log("a:", a());
console.log("b:", b());
Output
a: 3
b: 1

Every call to makeCounter creates a fresh count. a and b each remember their own.

Private state

Variables captured by a closure can't be reached from outside, which makes closures a simple way to hide data:

JS private.js
function createWallet(initial) {
  let balance = initial;

  return {
    deposit(amount) {
      if (amount <= 0) throw new Error("Deposit must be positive");
      balance += amount;
    },
    getBalance() {
      return balance;
    },
  };
}

const wallet = createWallet(50);
wallet.deposit(25);
console.log(wallet.getBalance());
console.log(wallet.balance);
Output
75
undefined

The only way to change balance is through deposit, which validates its input. This "module pattern" was the main way to get privacy before classes had #private fields (lesson 40).

Function factories

A function that builds and returns customised functions:

JS factory.js
function multiplier(factor) {
  return n => n * factor;
}

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

console.log(double(5), triple(5));
console.log([1, 2, 3].map(multiplier(10)));
Output
10 15
[ 10, 20, 30 ]

Closures in the browser

Every event handler that uses an outer variable is a closure. Here each button's handler remembers its own count and label:

Try it yourself
Result

Practical closure patterns

Run only once

JS once.js
function once(fn) {
  let done = false;
  let result;
  return (...args) => {
    if (!done) {
      done = true;
      result = fn(...args);
    }
    return result;
  };
}

const init = once(() => {
  console.log("initialising…");
  return "ready";
});

console.log(init());
console.log(init());
Output
initialising…
ready
ready

Memoisation (caching results)

JS memo.js
function memoize(fn) {
  const cache = new Map();
  return n => {
    if (!cache.has(n)) cache.set(n, fn(n));
    return cache.get(n);
  };
}

let calls = 0;
const slowSquare = n => { calls++; return n * n; };
const fastSquare = memoize(slowSquare);

fastSquare(9); fastSquare(9); fastSquare(9);
console.log(fastSquare(9), "computed", calls, "time(s)");
Output
81 computed 1 time(s)

Debouncing — waiting until a user stops typing before searching — is another classic closure, shown in lesson 47.

A note on memory

As long as a closure exists, the variables it captured stay in memory. That's usually exactly what you want. It only becomes a problem if you keep closures alive by accident — for example, event listeners on elements you've removed — while they hold on to large data.

Try it yourself

Try it yourself
Console output