Most explanations of closures start with a definition that sounds like it was written for a compiler, not a person: "a closure is the combination of a function bundled together with references to its surrounding state." That sentence is accurate and almost useless the first time you read it. Here's a version that actually sticks.
A closure is a function that remembers where it was born. When you create a function inside another function, the inner function keeps a working connection to the variables that were around it at creation time — even after the outer function has finished running and, by all appearances, should have cleaned everything up.
The smallest possible example
function makeGreeter(name) {
return function () {
console.log("Hello, " + name);
};
}
const greetAda = makeGreeter("Ada");
const greetGrace = makeGreeter("Grace");
greetAda(); // "Hello, Ada"
greetGrace(); // "Hello, Grace"
By the time greetAda() runs, makeGreeter has already returned. Its local scope should, in theory, be gone. But the inner function still has access to name, because it closed over that variable when it was created. Each call to makeGreeter creates a brand-new, separate name, so greetAda and greetGrace never step on each other.
That's the whole mechanism. Everything else is just this idea showing up in different clothes.
Where this actually matters: three real cases
1. Counters that don't leak state
Before closures click, it's tempting to reach for a global variable to track something like a click count. Closures let you keep that state private to exactly the function that needs it:
function createCounter() {
let count = 0;
return {
increment: () => ++count,
reset: () => (count = 0),
value: () => count,
};
}
const counter = createCounter();
counter.increment();
counter.increment();
counter.value(); // 2
There is no way to reach count directly from outside — no counter.count, no accidental overwrite from some unrelated part of the app. The only way in is through the functions that were defined alongside it. This is closures doing the job that a "private" keyword does in other languages.
2. Event handlers that remember context
Say you're rendering a list of buttons and each one needs to know which item it belongs to:
function attachHandlers(items) {
items.forEach((item, index) => {
const button = document.querySelector('#item-' + index);
button.addEventListener('click', function () {
console.log('You clicked:', item.name);
});
});
}
Each click handler closes over its own item, captured at the moment the handler was created inside the loop. Long after attachHandlers has finished, each button still "remembers" exactly which item it was assigned — this is the same mechanism as the greeter example, just wired up to the DOM instead of a return value.
var-in-a-loop bug comes from. With var, all iterations of a loop share one variable, so every handler ends up closing over the same final value. Switching to let gives each iteration its own binding, which is why modern code rarely hits this bug anymore.
3. Memoization: caching expensive work
Closures are also how you build a cache that lives alongside a function, without polluting the outer scope:
function memoize(fn) {
const cache = new Map();
return function (arg) {
if (cache.has(arg)) return cache.get(arg);
const result = fn(arg);
cache.set(arg, result);
return result;
};
}
const slowSquare = (n) => { /* pretend this is expensive */ return n * n; };
const fastSquare = memoize(slowSquare);
fastSquare is a closure over cache and fn. Every call checks the private cache first, so repeated calls with the same input skip the expensive work entirely — and nothing outside memoize can see or tamper with that cache.
Why this is worth understanding, not just memorizing
You'll use closures constantly whether or not you ever think about the word: every React useState setter, every debounce or throttle utility, every callback passed to setTimeout relies on this same behavior. Once the "remembers where it was born" framing clicks, a lot of code that used to look like magic starts looking like ordinary cause and effect.
The mental model to keep is simple: a function carries its birth scope with it, wherever it's called from later. Everything else — private state, stable callbacks, caching — is just a consequence of that one fact.