JavaScript · Intermediate
Closures in JavaScript
In short: A closure is a function together with the scope it was created in. In JavaScript every function is a closure: it can read and update the variables of its enclosing scopes even after those scopes have finished running, which is how a returned inner function keeps a private counter and how a callback remembers the data it was set up with.
A function that carries its surroundings
When a function is created, it does not only store its code. It also keeps a reference to the scope it was written in, and through that scope to every enclosing scope out to the global one. That bundle of function plus captured scope is a closure. The scope lesson explained that a name is looked up outwards through the enclosing scopes; a closure is what makes that lookup still work when the inner function runs later, somewhere else entirely.
The interesting case is an inner function that outlives the call that created it. makeCounter declares let count = 0 and returns a function that increments it. Normally a function's local variables disappear when it returns. Here they cannot, because the returned function still refers to count, so the engine keeps that scope alive for as long as the returned function exists. Each call to makeCounter creates a fresh scope with its own count, which is why two counters made by the same factory do not interfere with each other.
A closure captures variables, not values. The inner function does not take a snapshot of count at creation time; it holds the variable itself and sees every later change to it. That is exactly what a counter needs, and it is also the source of the classic loop surprise: three functions created inside for (var i = 0; ...) all share the one function-scoped i and all report its final value. With let, each iteration gets its own binding of i, so each function captures a different variable and reports the value from its own iteration.
Because the captured variables are reachable only through the returned function, a closure is JavaScript's simplest form of private state. Nothing outside makeCounter can read or reset count except by calling the function that closes over it. Before classes had private fields this was the standard way to hide data, and it is still the most direct one when a single function is all the interface you need.
The same mechanism explains function factories and partial application. makeDiscounter(20) computes a factor once and returns a function that applies it; calling the factory with a different rate gives an independent function with its own captured rate. Callbacks work the same way: an event handler or a setTimeout callback written inside a function can use that function's parameters and locals when it eventually runs, with no need to pass them again.
Closures keep their captured scope alive, so they hold memory for as long as they are reachable. That is rarely a problem, but a long-lived callback that closes over a large array keeps the whole array in memory even if it only needs its length. Capture what you need, and drop references to closures you no longer use.
Syntax
function outer() {
let hidden = 0; // lives in outer's scope
return function inner() { // inner closes over hidden
hidden += 1;
return hidden;
};
}
const step = outer(); // outer has returned...
step(); // ...but hidden is still alive: 1
step(); // 2
const makeAdder = (n) => (x) => x + n; // arrow functions close over n too
const addFive = makeAdder(5);There is no special keyword. Any function that refers to a variable from an enclosing scope is a closure over that variable.
Private counters from one factory
makeCounter returns a function that increments a variable nobody else can reach. Two counters are created; notice that they count separately and that count is invisible outside.
function makeCounter(label) {
let count = 0;
return function () {
count += 1;
return `${label}: ${count}`;
};
}
const tickets = makeCounter("ticket");
const visitors = makeCounter("visitor");
console.log(tickets());
console.log(tickets());
console.log(visitors());
console.log(tickets());
console.log(typeof count);Output
ticket: 1 ticket: 2 visitor: 1 ticket: 3 undefined
Every call to makeCounter runs its body afresh, creating a new count and a new label, and the returned function captures those particular variables. tickets and visitors therefore have separate counts. After makeCounter returns, its scope survives because the returned function still refers to it. The last line proves the variable is private: at the top level count does not exist, so typeof count is "undefined".
A function factory with a captured rate
makeDiscounter computes a multiplier once and returns a function that applies it to any price. Prices are read from input and priced for two customer groups.
function makeDiscounter(percent) {
const factor = 1 - percent / 100;
return function (price) {
return Math.round(price * factor * 100) / 100;
};
}
const staff = makeDiscounter(20);
const member = makeDiscounter(5);
let line;
while ((line = readline()) !== null) {
const price = Number(line);
console.log(`${price} -> staff ${staff(price)}, member ${member(price)}`);
}Input given to the program: 50 ↵ 12.5 ↵ 99.99
Output
50 -> staff 40, member 47.5 12.5 -> staff 10, member 11.88 99.99 -> staff 79.99, member 94.99
factor is calculated when the factory is called and then captured by the returned function, so staff always multiplies by 0.8 and member by 0.95 without recomputing or being told the rate again. The two functions share the same code but close over different scopes. Rounding to two decimals is done inside, so callers get a finished price.
Closures capture variables, not values
Functions created in a var loop all see the same counter; functions created in a let loop each see their own. The last block shows a closure observing a later change.
const withVar = [];
for (var i = 0; i < 3; i++) {
withVar.push(function () { return "shelf " + i; });
}
const withLet = [];
for (let k = 0; k < 3; k++) {
withLet.push(function () { return "shelf " + k; });
}
console.log(withVar.map(fn => fn()));
console.log(withLet.map(fn => fn()));
let temperature = 18;
const report = () => `current: ${temperature}`;
temperature = 23;
console.log(report());Output
[ 'shelf 3', 'shelf 3', 'shelf 3' ] [ 'shelf 0', 'shelf 1', 'shelf 2' ] current: 23
There is only one var i, and by the time the functions are called the loop has finished and left it at 3, so all three report the same shelf. A let loop variable is rebound on every iteration, so each function captured a different k holding 0, 1 or 2. The final block makes the rule explicit: report was created while temperature was 18, but it reads the variable at call time and sees 23.
Common mistakes
Expecting a closure to freeze a value at creation time
Why it goes wrong: The inner function holds the variable, not a copy. If the variable changes before the inner function runs, the inner function sees the new value. Loops with
varare the usual place this bites.Fix: Use
letin loops so each iteration has its own variable, or pass the value into a factory function whose parameter captures it.JavaScript · fixfor (let i = 0; i < 3; i++) { handlers.push(() => console.log(i)); // 0, 1, 2 }Sharing state between instances by accident
Why it goes wrong: If the variable is declared outside the factory, every function the factory returns closes over the same variable, and the "independent" counters all increment one number.
Fix: Declare the state inside the factory so each call creates a fresh scope.
Holding large data alive through a long-lived callback
Why it goes wrong: A closure keeps its captured scope reachable. A handler that references a huge array keeps that array in memory for as long as the handler is registered, even if it only used the array once.
Fix: Compute what the callback needs before creating it and capture only that, and remove listeners or timers you no longer need.
Reaching for closures where a plain argument would do
Why it goes wrong: A factory that returns a function only to be called once adds indirection without benefit, and the captured state is harder to inspect than a value passed openly.
Fix: Use a closure when something must persist between calls or be hidden; otherwise pass the data as a parameter.
Where you use this
Closures are everywhere in event-driven code. A click handler written inside the function that built a button can use that function's local item when the click happens minutes later, so a list of buttons can each know which row they belong to without any global lookup table. Timers and promise callbacks rely on the same guarantee.
They are also the tool for remembering results. A memoised function keeps a cache in the scope of the factory that created it: the returned function checks the cache, and nothing else can corrupt it. Debounce and throttle helpers, which hold a pending timer between calls, are closures too. The modules lesson shows the module pattern, where a whole set of functions shares one private scope, and the classes lesson shows private fields, which cover the object-shaped cases.
function memoise(fn) {
const cache = new Map();
return function (arg) {
if (!cache.has(arg)) cache.set(arg, fn(arg));
return cache.get(arg);
};
}Key points
- A closure is a function plus the scope it was created in; every JavaScript function is one.
- Variables of an outer function stay alive as long as an inner function that uses them is reachable.
- Each call to a factory creates a fresh scope, so the functions it returns have independent state.
- Closures capture variables, not values: a later change to the variable is visible to the inner function.
- In loops,
letgives each iteration its own binding;vargives every closure the same one. - Captured state is private: only the closure can reach it.
- Captured scopes cost memory for as long as the closure lives.
Try it yourself
Complete makeAccumulator so it returns a function that adds its argument to a running total that starts at start and returns the new total. The three calls should print 105, 125 and 100.
function makeAccumulator(start) {
// return a function that adds its argument to a running total
}
const add = makeAccumulator(100);
console.log(add(5));
console.log(add(20));
console.log(add(-25));105
125
100function makeAccumulator(start) {
let total = start;
return function (amount) {
total += amount;
return total;
};
}
const add = makeAccumulator(100);
console.log(add(5));
console.log(add(20));
console.log(add(-25));Practise this
Exercises for this lesson are in the JavaScript practice set.
Open the JavaScript playground
Related lessons
- Scope and Hoisting in JavaScriptHow JavaScript decides where a variable is visible: global, function and block scope, how var and function declarations hoist, and why let has a dead zone.10 min
- JavaScript Functions: Declarations, Arrows and Return ValuesHow to define and call JavaScript functions with declarations, expressions and arrow functions; parameters, default values, return and local scope.10 min
- Modules in JavaScriptHow JavaScript ES modules split a program into files: named and default exports, import forms, module scope, single evaluation and the contrast with CommonJS.10 min
- Classes in JavaScriptHow JavaScript classes work: constructors and this, methods on the prototype, getters, static members, extends and super, and private fields with #.11 min
Frequently asked questions
What is a closure in JavaScript in simple terms?
A function that remembers the variables around it. When you create a function inside another function and use the outer function's variables, the inner function keeps access to them even after the outer function has returned. That is why a returned counter function can keep counting: the variable it counts with is still there, held by the closure.
Are arrow functions closures too?
Yes. Arrow functions capture enclosing variables exactly like function expressions do. The only differences are that an arrow function has no this or arguments of its own, taking both from the enclosing scope, and that it cannot be used as a constructor.
Do closures cause memory leaks?
Not by themselves. A closure keeps its captured scope alive only while the closure is reachable; once nothing refers to the function, both it and the scope can be garbage collected. Leaks happen when a closure is registered somewhere permanent, such as an event listener that is never removed, and it captures data that is no longer needed.
How this page was checked. Every program on it was run with Node.js 22 at build time; runs in your browser in an isolated Web Worker by the publishing checks, and the output shown is what it printed. The Run buttons execute the same code on your device; nothing you type is sent anywhere.