A closure is a function that retains access to its outer scope even after that scope has finished executing. Understanding closures unlocks async patterns, module patterns, and half of what interviewers actually test.
closuresscopefunctionsmemory
Knowledge Check
6 questions · pass at 70%
0/6
easypredict output
1. What does this log?
code
function makeCounter() {
let count = 0;
return () => ++count;
}
const a = makeCounter();
const b = makeCounter();
console.log(a(), a(), b());
Interview Questions
2 questions
Cheatsheet
Download-ready reference
Cheatsheet
Closure = a function + a live reference to its lexical environment
(not a copy of values — a reference to the actual bindings).
Created: every time a function is defined inside another function.
Stays alive: as long as something reachable still references the
inner function (returned, stored, passed as a callback, etc.).
var in a loop → ONE shared binding → closures all see the FINAL value
let in a loop → a FRESH binding per iteration → closures see their own value
Module pattern:
function make() {
let private_ = 0; // unreachable from outside
return { get: () => private_, set: (v) => { private_ = v; } };
}
React: every render = a new closure over that render's props/state.
Stale closure bug = an old render's closure (effect/timeout/listener)
still running after a re-render, reading OLD values forever.
Fix: correct dependency array, or functional state updates (c => c + 1).
Debug memory retention: Chrome DevTools → Memory → Heap Snapshot → search "Closure".
The beginner framing: when a function is defined inside another function, it "closes over" the variables in the outer function — capturing them by reference, not by value.
The precise mental model: a closure is the combination of a function and its lexical environment — the set of variable bindings that were in scope at the point the function was created. The V8 engine represents this as a hidden [[Environment]] slot on every function object, pointing to the scope chain at creation time.
function makeCounter() { let count = 0; // captured by the closure below return function increment() { count += 1; return count; };}const counter = makeCounter();counter(); // 1counter(); // 2counter(); // 3
makeCounter has returned. Its stack frame is gone. But count is still alive because increment holds a reference to the scope that contains it. The GC won't collect it until all references to increment are dropped.
How makeCounter() creates a closure
step 1 / 5
function makeCounter() {
let count = 0;
return function increment() {
count += 1;
return count;
};
}
const counter = makeCounter();
counter(); // ?
counter(); // ?
Global
makeCounter: ƒ
counter: (uninitialized)
makeCounter()
count: 0
makeCounter() runs. count = 0 lives in its own function scope.
Edge case: closures capture the binding, not the value#
const fns = [];for (var i = 0; i < 3; i++) { fns.push(() => console.log(i));}fns[0](); // 3 — not 0!fns[1](); // 3fns[2](); // 3
var is function-scoped, so all three closures share the same i binding. By the time any function runs, the loop has finished and i === 3.
Fix with let (block-scoped — a fresh binding per iteration) or an IIFE:
for (let i = 0; i < 3; i++) { fns.push(() => console.log(i)); // 0, 1, 2 ✓}
function makeMultiplier(factor) { return function (n) { return n * factor; };}const double = makeMultiplier(2);const triple = makeMultiplier(3);console.log(double(5));
Solution
101520
double and triple are two entirely separate closures — each call to makeMultiplier creates a brand-new scope with its own factor binding. They don't share state, even though they were created by the same function. This is what makes closures useful as a "function factory": each returned function is independently configured.
Build a once utility — a function that wraps another function so it can only ever run one time, using a closure to remember whether it already has:
function once(fn) { let called = false; let result; return function (...args) { if (!called) { result = fn(...args); called = true; } return result; };
called and result live in a scope that only the returned function can reach — there is no once.called property to accidentally reset from outside. This is the same private-state pattern behind useState, memoization utilities, and one-time setup guards you'll find throughout real codebases.
Every function component body runs fresh on every render, and every function you define inside it — event handlers, effect callbacks, memoized functions — is a new closure over that specific render's props and state. This isn't incidental; it's the entire mechanism that makes hooks work at all:
function SearchBox({ onSearch }) { const [query, setQuery] = useState(""); function handleSubmit() { onSearch(query); // closes over THIS render's `query` } return <button onClick={handleSubmit}>Search</button>;}
handleSubmit doesn't read query from some shared mutable box — it closes over the exact query value from the render that created it. Click the button, and you get that render's query, guaranteed, even if a re-render with a different query happens moments later. This is also precisely why stale closures (see Common Mistakes below) exist as a category of React bug: a closure created in an earlier render, still alive because something (an effect, a timeout, an event listener) is holding onto it, will keep seeing that earlier render's values forever — it has no way to "catch up" to newer state.
The entire heavyData object is kept alive as long as the event listener exists — even if you only need one property. Fix: close over only what you need.
function attachHandler(element) { const name = loadHeavyData().name; // capture only what's needed element.addEventListener("click", () => console.log(name));}
3. Treating closure state as private when it isn't#
Closures give you encapsulation, not true privacy. The inner variable is inaccessible from outside — but whoever receives the function controls when and how often it runs, so "private" is relative to the caller's control.
Capture the minimum. Extract only the values you need rather than letting large objects stay alive through a closure.
Name your inner functions. Anonymous arrow functions make stack traces harder to read. const increment = () => ... is better than a nameless () => ... in a returned position.
Prefer let/const over var. Block scoping eliminates the loop-closure gotcha by default.
In React: audit your useEffect and useCallback dependency arrays — every variable from outer scope used in a callback is a potential stale closure.
Closures themselves have near-zero overhead. The hidden [[Environment]] pointer is a single pointer indirection.
The cost is in retained memory. If a closure is long-lived (event listener, timer, cached function), the entire captured scope chain stays on the heap. Profile with Chrome DevTools heap snapshots — search for "Closure" in the heap summary to see what's retained and by what.
V8 optimizes "dead" captured variables away via escape analysis — if a closure captures x but never reads it, V8 may not allocate the closure slot. Don't rely on this for correctness, but it's why micro-benchmarks of trivial closures look faster than expected.