JavaScript Closures Explained With Examples You Will Actually Meet
What a closure is, why the classic loop bug happens, and the three real-world patterns closures enable — memoisation, private state and partial application.
Table of contents
A closure is a function that remembers the variables from where it was defined, not where it is called. That is the entire concept. Everything else is a consequence.
function counter() {
let count = 0; // lives in counter's scope
return () => ++count; // but this function keeps a reference to it
}
const next = counter();
next(); // 1
next(); // 2count should have been garbage-collected when counter returned. It was not, because the returned function still holds a reference to the scope containing it. That surviving scope is the closure.
The classic loop bug#
This is the example every tutorial uses, and it is worth understanding rather than memorising:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// logs 3, 3, 3var is function-scoped, so all three callbacks close over the same i. By the time the timeouts run, the loop has finished and i is 3.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// logs 0, 1, 2let creates a fresh binding per iteration, so each callback closes over its own i. This is the single most practical reason to never use var.
Pattern 1: private state#
Closures give you encapsulation without classes:
function createRateLimiter(limit, windowMs) {
const hits = []; // not reachable from outside
return function allow() {
const now = Date.now();
while (hits.length && now - hits[0] > windowMs) hits.shift();
if (hits.length >= limit) return false;
hits.push(now);
return true;
};
}There is no way for a caller to tamper with hits. A class with a #private field achieves the same thing; the closure version is smaller when you only need one method.
Pattern 2: memoisation#
function memoise(fn) {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn(...args);
cache.set(key, result);
return result;
};
}The cache lives as long as the returned function does — which is also the memory-leak risk worth flagging: an unbounded cache in a long-lived closure grows forever. Use a bounded cache, or a WeakMap when the key is an object.
Pattern 3: partial application#
const log = (level) => (message) => console.log(`[${level}] ${message}`);
const warn = log('warn');
warn('disk almost full'); // [warn] disk almost fullEach returned function closes over its own level.
The memory consideration#
Closures keep their entire enclosing scope alive, not just the variables they use — engines optimise this, but not always completely. The practical rule: do not close over a large object you do not need.
function attach(hugeData) {
const id = hugeData.id; // extract what you need
return () => console.log(id); // closes over id, not hugeData
}Frequently asked questions#
Is every JavaScript function a closure?#
Technically yes — every function closes over its defining scope. The term is normally reserved for cases where that matters: a function that outlives the scope it captured.
Do closures cause memory leaks?#
They can, when a long-lived closure retains something large. The common real case is an event listener that captures a DOM node: until the listener is removed, the node cannot be collected.
Are closures slow?#
No. Variable access through a closure is essentially as fast as a local, and modern engines optimise the common shapes aggressively.
How is a closure different from a class?#
A class groups several methods around shared state declared up front. A closure gives you one function (or a small returned object) with hidden state. Prefer a closure for one behaviour, a class for several.
Related reading#
- The JavaScript Event Loop — why the
setTimeoutexample logs after the loop - React's useMemo and useCallback — closures are the reason stale-value bugs happen in hooks