JavaScript Closures
debt(d7/e3/b3/t7)
Closest to 'only careful code review or runtime testing' (d7). ESLint can catch the classic `var` in loop pattern with certain rules (e.g. no-loop-func), but the broader closure misuse — capturing references instead of values, memory leaks from large objects held in closure scope — requires careful code review or runtime/heap profiling to surface. The detection_hints list ESLint and TypeScript as tools, but they only catch the narrowest pattern, leaving many closure bugs invisible until runtime.
Closest to 'simple parameterised fix' (e3). The quick_fix cites replacing `var` with `let`/`const` inside loops, which is a direct pattern replacement within a localized area of code. Memory leak issues may require a bit more thought (nulling references, restructuring callbacks), but the fix is generally contained to one component rather than cross-cutting.
Closest to 'localised tax' (b3). Closures are a pervasive JavaScript concept but the debt from misuse is typically localized — loop closure bugs affect the specific loop or callback, and memory issues affect the specific module or component. The applies_to covers web and cli broadly, but the structural weight falls on individual features or components rather than shaping the whole codebase.
Closest to 'serious trap' (t7). The misconception field directly states developers believe closures are a special function type, when in fact every function is a closure. Combined with the common_mistakes — especially capturing variable references not values, and the classic var-in-loop bug — this contradicts intuition from other languages where loop variables are naturally scoped per iteration. This is a well-documented gotcha that trips up developers moving from other languages and even experienced JS developers.
Also Known As
TL;DR
Explanation
A closure is created every time a function is defined — it captures the surrounding lexical environment (variables in scope at definition time). This enables: private state (a counter variable accessible only through returned increment/decrement functions), partial application, module patterns, and event handler factories. The classic loop bug: for(var i=0;i<3;i++) setTimeout(()=>console.log(i),0) logs 3 three times because all closures share the same var i. Fix: use let (block-scoped) or an IIFE wrapper. PHP closures (anonymous functions) work similarly but require explicit use($var) to capture outer variables — they don't capture automatically.
Diagram
flowchart TD
OUTER[Outer function<br/>let count = 0] --> INNER[Inner function<br/>returns count++]
INNER --> CLOSURE[Closure<br/>captures count variable<br/>not its value]
CLOSURE --> EX1[call counter - returns 1]
EX1 --> EX2[call counter - returns 2]
EX2 --> EX3[call counter - returns 3]
subgraph Common Use Cases
PRIV[Private state - module pattern]
CB[Callbacks with context]
PARTIAL[Partial application]
MEMO[Memoisation cache]
end
style OUTER fill:#6e40c9,color:#fff
style CLOSURE fill:#238636,color:#fff
style EX3 fill:#1f6feb,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- The classic loop closure bug — var in a for loop shares the same variable across all closures.
- Creating closures in tight loops that hold large objects — memory stays allocated as long as closures exist.
- Not understanding that closures capture the variable reference, not its value at creation time.
- Using closures for private state when class private fields (#field) are more explicit.
Code Examples
// Classic loop closure bug with var:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // Logs 3, 3, 3 — all share same 'i'
}
// Fixed with let (block scope) or IIFE:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // Logs 0, 1, 2 — each iteration has own 'i'
}
function makeCounter() {
let count = 0; // private — not accessible outside
return {
inc: () => ++count,
get: () => count,
};
}
const c = makeCounter();
c.inc(); c.inc();
console.log(c.get()); // 2