A closure doesn't capture the one variable you meant to use; it captures a strong reference to its entire outer lexical environment.
Left unmanaged, that environment never leaves memory.
The dashboard was a complex data grid, the kind of tool an operations team keeps open all day.
It worked perfectly for the first few minutes of every session.
After about twenty minutes of continuous use, it started to grind filters lagged, scrolling stuttered, and eventually the whole view became sluggish enough that people started refreshing the page out of habit rather than diagnosis.
The DOM size was normal.
The network requests were fine.
The actual cause was an asynchronous polling loop, set up once when the component first mounted, that closed over the component's initial state.
It was never cleaned up or resynchronized.
Every poll cycle, it dutifully updated the UI using a reference to data that was frozen at the exact millisecond the component first rendered, and it held onto that original state object in memory for the entire session, growing more stale and more expensive with every passing minute.
The engineering team's first instinct was to blame the framework's rendering performance.
The actual root cause was a fundamental property of closures that most engineers learn as a feature and never revisit as a liability: a closure does not capture the single variable you intended to use.
It captures a reference to its entire enclosing lexical scope, and that scope remains pinned in memory completely immune to garbage collection for as long as the closure itself is reachable.
Closures are JavaScript's superpower.
They enable encapsulation, private state, and clean module patterns that predate every modern framework.
In reactive, event-driven architectures where components mount and unmount continuously and callbacks live far longer than the code that created them, that same superpower is the leading cause of stale state bugs and quiet memory bloat.
The mechanism: what a closure actually holds onto When a function is defined inside another function, it forms a closure over its enclosing scope not just the variables it references, but the entire lexical environment in which it was created.
The JavaScript engine cannot selectively retain "just the parts you're using." It retains the whole environment record, because any part of it could theoretically still be accessed. and are never referenced inside , but because they exist in the same lexical scope as , V8 cannot prove they are unreachable, and in practice engines commonly retain the whole scope record rather than performing fine-grained per-variable analysis.
If is registered as a callback or a long-lived event listener, every object in that outer scope remains alive for as long as the interval runs, which, if nobody explicitly clears it, is the lifetime of the page.
This is not a bug in the engine.
It is the correct, specified behavior of lexical scoping.
The problem is architectural: nobody decided how long and everything it drags with it should actually live.
The real-world cost: stale state and memory bloat, together The dashboard incident above illustrates both failure modes closures produce simultaneously, because they share the same root cause.
Stale state synchronization bugs.
A callback registered during an initial render captures the state variables from that specific execution context.
In frameworks with reactive re-rendering, the component re-renders with fresh state on every update, but the previously registered callback does not automatically know about it.
It continues operating on the closed-over values from whenever it was created.
If updates frequently but the effect's dependency array only includes , the interval closure keeps referencing the value from whenever the effect last ran not the current one. silently operates on old data indefinitely.
This is precisely the bug that produces "it works when I test it manually but breaks after a few minutes of real use" reports
