Closures

Imagine a function packed with a little backpack. Wherever the function goes — stored in a variable, passed around, called much later — it carries that backpack, and inside are the variables from the place it was born. A closure is exactly this: a function together with the environment it was created in. It "closes over" the variables around it, keeping them alive even after the code that made them has finished running.

Closures are what make higher-order functions truly powerful. Once a function can remember values from where it was made, you can build function factories — functions that manufacture other, specialised functions on demand.

A function that remembers

\text{makeAdder} takes a number n and returns a brand-new function. That returned function uses n — even though makeAdder has already returned and, you'd think, n should be long gone. The closure keeps n in the backpack.

function makeAdder(n: number): (x: number) => number { return (x: number) => x + n; // this inner function "closes over" n } const addFive = makeAdder(5); // n = 5 is captured in addFive's backpack const addTen = makeAdder(10); // a separate backpack, n = 10 console.log(addFive(100)); // 105 console.log(addTen(100)); // 110 console.log(addFive(1)); // 6 — addFive still remembers its own n

Two calls to makeAdder made two independent closures, each with its own private n. They don't share; they don't interfere. That private, remembered value is the heart of the idea.

Function factories: making specialists

Once you can bake a value into a function, you can stamp out families of related functions from one template. Here a single \text{power} factory produces a squarer, a cuber, and a "to the tenth" — each remembering its own exponent.

function power(exp: number): (base: number) => number { return (base: number) => base ** exp; // exp lives in the closure } const square = power(2); const cube = power(3); const tenth = power(10); console.log("square(6) =", square(6)); // 36 console.log("cube(4) =", cube(4)); // 64 console.log("tenth(2) =", tenth(2)); // 1024

This is the same trick a configured function uses everywhere: set the knobs once, get back a ready-to-use function you can call again and again without re-passing the settings.

A closure can capture a variable it is allowed to change, giving you private state that nothing outside can touch — a tiny bit of encapsulation without a class. Each call to makeCounter gets its own hidden count:

function makeCounter(): () => number { let count = 0; // private — nobody outside can reach it return () => { count += 1; return count; }; } const tick = makeCounter(); console.log(tick(), tick(), tick()); // 1 2 3 const other = makeCounter(); console.log(other()); // 1 — its own separate count

Purists note this closure is stateful — calling tick() twice gives different answers, so it isn't a pure function. That's fine: closures are a tool, and here we're using them deliberately to hold state. Most of the time, though, you'll close over values you only read, keeping things pure.

Why closures matter

The famous closure gotcha is capturing a loop variable. A closure remembers the variable, not a snapshot of its value at creation time — so if the variable keeps changing, every closure sees the final value. With the old function-scoped var, all the closures below share one i:

const bad: (() => number)[] = []; for (var i = 0; i < 3; i++) { bad.push(() => i); // all capture the SAME i } console.log(bad.map((f) => f())); // [3, 3, 3] — oops! const good: (() => number)[] = []; for (let j = 0; j < 3; j++) { good.push(() => j); // let gives each iteration its OWN j } console.log(good.map((f) => f())); // [0, 1, 2] — fixed

The fix is to give each iteration a fresh binding — which is exactly what block-scoped let (or const) does. This one bug has bitten every JavaScript programmer at least once; now it won't bite you.