Closures & Memory
Introduction
Closures are not just a syntax feature; they affect memory. If a function closes over a variable and that function is still reachable, the captured variable must remain reachable too.
That is usually exactly what you want — a counter remembering its count — but it can become a memory leak when long-lived callbacks accidentally retain large objects.
Why This Matters
Senior JavaScript interviews often move from "what is a closure?" to "what does it keep alive?". Understanding closure memory helps you debug leaks caused by event handlers, timers, caches, and stale callbacks in long-running applications.
Theory
Closures keep reachable bindings alive
JavaScript garbage collection is based on reachability. If an object can be reached from roots such as the global object, the call stack, active timers, event listeners, or module state, it cannot be collected.
A reachable closure points to its captured lexical environment. Any captured variable that can still be used by the closure must stay alive. Engines can optimise details, but the semantic rule is simple: if reachable code can observe it later, it cannot disappear.
A closure does not always mean a leak
Most closures are short-lived and cheap. A closure becomes a leak risk when it is held for longer than intended and captures more data than necessary. Common examples:
| Long-lived holder | Captured by closure | Leak risk |
|---|---|---|
| Event listener | large object or component state | listener not removed |
setInterval callback | old data snapshot | interval not cleared |
| Unbounded memoization cache | many results | cache grows forever |
| Global array of callbacks | request/user objects | callbacks never removed |
When is closure memory freed?
The captured environment can be collected when no reachable closure can use it anymore. Practically, that means you remove event listeners, clear timers, delete callbacks from registries, or let the returned function itself become unreachable.
You can also release heavy captured data earlier by assigning the captured variable to null when it is no longer needed. The closure may remain, but it no longer retains the large object through that variable.
Avoid overclaiming
In interviews, avoid saying "closures keep the whole stack frame forever." The accurate model is: closures keep the necessary lexical bindings reachable. Engines may optimise unused variables, but observable captured values must behave as if they remain available.
Visual Diagrams
GC roots | v active interval / listener | v callback function (closure) | hidden lexical link v captured environment | v large object or stale state Clear the interval/listener or clear the captured reference to allow collection.
Garbage collection follows reachability. A reachable callback can keep its captured data alive.
Code Examples
A timer closure retains what it uses
The interval callback keeps items reachable until the interval is cleared or the callback becomes unreachable.
Release a heavy captured value explicitly
If the closure must remain alive but no longer needs the heavy data, clear the captured reference.
Output Prediction
Predict the output #1
1function makeReader() {2 let data = ['large', 'payload'];3 4 return {5 read: function () {6 return data ? data.length : 0;7 },8 release: function () {9 data = null;10 }11 };12}13 14const reader = makeReader();15console.log(reader.read());16reader.release();17console.log(reader.read());Interview Questions
1Do closures cause memory leaks?
Closures do not inherently cause leaks. They keep captured variables alive while the closure is reachable, which is correct behavior. A leak happens when a closure is unintentionally kept alive for too long — for example by an uncleared interval, an event listener that was not removed, or an unbounded cache — and it retains large data that should have been released.
Follow-ups
- How would you fix an event-listener leak?
- Can a captured variable be cleared while the closure remains?
2When can the memory captured by a closure be garbage-collected?
When it is no longer reachable. If no reachable function can access the captured environment, the environment can be collected. You can make that happen by dropping references to the closure, removing listeners, clearing timers, deleting callbacks from registries, or clearing captured references such as setting a large object variable to null.
Quiz
1. A closure captures a large array. When can that array be garbage-collected?
2. Which pattern is most likely to leak memory?
Summary
- Reachable closures keep their captured lexical bindings reachable.
- Closures are not leaks by default; leaks come from closures that live longer than intended.
- Timers, listeners, registries, and unbounded caches are common retention sources.
- Free memory by removing holders, dropping closure references, bounding caches, or clearing captured heavy values.