Closures
A function plus its lexical environment — scope chains, factories, privacy patterns, loop gotchas, and memory.
- closures
- scope
- functions
A closure is a function bundled with the lexical environment where it was created — the outer variables it can still reach.
In JavaScript, every function is a closure. People only start using the word when that outer environment outlives the function that created it: you return an inner function, attach a handler, pass a callback, and somehow the old locals are still there.
That’s not a snapshot of values. It’s a live link to bindings (the boxes), not the numbers that happened to sit in those boxes yesterday.
Lexical scope first
function init() {
const name = 'Mozilla';
function displayName() {
console.log(name); // outer binding
}
displayName();
}
init();
displayName has no local name. The engine walks outward and finds it. “Lexical” just means: scope is decided by where you wrote the function in the source, not by who called it.
var is function-scoped (or global). let / const are block-scoped. Closures can capture any of those.
The part that feels like a trick
function makeFunc() {
const name = 'Mozilla';
function displayName() {
console.log(name);
}
return displayName;
}
const myFunc = makeFunc();
myFunc(); // still prints Mozilla
In many languages, locals die when the outer function returns. In JS, if anything still references the inner function, the environment it closed over stays alive. myFunc is displayName plus the environment where name lives.
Factories: same body, different backpacks
function makeAdder(x) {
return function (y) {
return x + y;
};
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);
add5(2); // 7
add10(2); // 12
Same function text, two environments: one where x is 5, one where x is 10. That’s why factories and partial application feel natural in JS.
Privacy without classes (still useful)
function makeCounter() {
let privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment() {
changeBy(1);
},
decrement() {
changeBy(-1);
},
value() {
return privateCounter;
},
};
}
const a = makeCounter();
const b = makeCounter();
a.increment();
a.value(); // 1
b.value(); // 0 — separate environments
Three methods share one lexical environment. Outside code can’t touch privateCounter except through the API you returned. Modern JS has private class fields now; the pattern still shows up in modules, hooks, and interview whiteboards.
The loop gotcha (say “binding,” not “value”)
function withVar() {
const fns = [];
for (var i = 0; i < 3; i++) {
fns.push(() => i);
}
return fns.map((f) => f());
}
// [3, 3, 3] — one shared `i`
function withLet() {
const fns = [];
for (let i = 0; i < 3; i++) {
fns.push(() => i);
}
return fns.map((f) => f());
}
// [0, 1, 2] — fresh binding per iteration
Closures didn’t “fail.” They correctly closed over the binding they were given.
Memory: capture what you need

A long-lived callback that closes over a fat object keeps that object alive. Grab the scalar you need and let the rest go.
function problem() {
const huge = new Array(1e6).fill('*');
return () => huge.length; // retains `huge`
}
function better() {
const huge = new Array(1e6).fill('*');
const len = huge.length;
return () => len; // retains a number
}
In React, “stale closure” usually means you read an old state in an effect or handler — fix with functional updates or correct deps, not by avoiding closures.
Everyday frontend
- Click handlers that need component-local config
- Debounced functions that remember timers
- Module-level “private” helpers
useEffectcleanups that close over the subscription they created
If you write UI, you already write closures. Naming the concept just helps you debug them.
Interview checklist
- Define it without the word “inner” as a crutch: function + lexical environment.
makeAdderor counter factory.varvsletin loops — shared binding.- Memory retention in one sentence.
Further reading
- Closures — MDN
- Variable scope, closure — javascript.info
- Closures interview angles — GreatFrontEnd quiz topics
Related
Related guides
- client offset scroll Dimensionsclient offset scroll Dimensions explained for frontend engineers — mental model, examples, common mistakes, and interview tips.
- The Event LoopHow JS runtimes schedule work — call stack, macrotasks, microtasks, rendering, and how to talk about it in interviews.
- PromisesSettlement, chaining, errors, Promise API helpers, and how promises plug into the microtask queue — without cargo-cult async.
- Pub Sub vs ObserverPub Sub vs Observer explained for frontend engineers — mental model, examples, common mistakes, and interview tips.
- getBoundingClientRectgetBoundingClientRect explained for frontend engineers — mental model, examples, common mistakes, and interview tips.