Closures and memory: how a small function can keep big data alive
A closure keeps its variables alive as long as it lives. See how forgotten listeners leak memory, a V8 detail that surprises people, and how to find leaks.
Comes up in: JavaScript, Web fundamentals
Learn#
Farhan’s dashboard gets slower every hour. Chrome’s Task Manager shows the tab growing from 80 MB to 900 MB. Every time a chart opens, it adds a resize listener. Charts close, listeners stay.
JavaScript frees memory with garbage collection: anything your program can no longer reach is cleaned up. A closure keeps its variables reachable for as long as the closure itself is reachable. That is called retention.
So a listener on a long-lived object — window, a global store, an event bus — keeps its closure alive, and the closure keeps alive every variable it uses. Memory that should be freed but is still reachable is a memory leak.
If that closure also references DOM nodes you removed, those “detached” nodes cannot be freed either.
event bus
empty
kept alive
empty
Call stack
- openChart
openChart() creates a big data array and a resize handler that uses it.
Read the steps as text
- openChart() creates a big data array and a resize handler that uses it.
- The handler is added to a global event bus. The bus now holds the handler, and the handler holds the data.
- The user closes the chart. The chart's DOM is removed, but nobody removed the handler.
- Open and close it 20 times: 20 handlers, 100 MB that the garbage collector cannot free.
- With cleanup, close() removes the handler. Nothing reaches the data, so it can be collected.
The fix is always the same: when the thing is gone, remove what points to it — removeEventListener, unsubscribe, clear the timer.
Predict#
Predict first
A component adds window.addEventListener('scroll', onScroll) on mount and never removes it. onScroll uses a 2 MB array. What happens after the component unmounts?
window lives as long as the page, it holds the listener, and the listener’s closure holds the array. Unmounting does not touch any of those links. Browsers only drop listeners when the target itself is collected — never for window.
Interview angle#
Say it like this — about 30 seconds
“Closures keep the variables they use alive while the closure is reachable. Leaks happen when something long-lived —
window, a global store, a timer, a cache — still holds a closure we no longer need, so its data and any detached DOM it references cannot be collected. The fix is cleanup: remove listeners, clear intervals, unsubscribe. To find leaks I take heap snapshots before and after repeating an action, and look at what grew and its retainers.”
Follow-up How do you find a memory leak in Chrome?
Follow-up Does a closure keep the whole outer scope alive?
Follow-up How do frameworks help?
ngOnDestroy; AbortController signals can remove many listeners at once.Practice#
On your own
openChart subscribes to a bus but close() does nothing. Make close() remove exactly the handler it added, so the chart’s data is no longer reachable.
Done when
- Before
close(), a “resize” event updateschart.width. - After
close(), the bus holds no handler, and callingclose()twice is safe. - Closing one chart does not remove another chart’s handler.
Remember#
Try to answer each card before you open it. Then add them to your reviews.
When can the garbage collector free an object?
When nothing your program can reach still references it.
How do closures cause memory leaks?
A long-lived object keeps a closure, and the closure keeps its variables alive.
What is a detached DOM node?
A node removed from the page but still referenced from JavaScript, so it cannot be freed.
How do you confirm a leak in Chrome DevTools?
Compare heap snapshots before and after repeating an action, and follow the retainers.
Go deeper
Four layers. Stop at the one your interview needs; each ends with a quick check.
The core ideaEvery round
Reachable means alive. A closure registered on something long-lived stays alive, and so does everything it uses. Clean up what you register.
Check yourself: Which outlives a component: window or the component's closure?
window. So a listener on window outlives the component unless removed.
How it works underneathSenior rounds
Collectors start from roots — globals, the call stack, live handles — and mark everything reachable from them; the rest is freed. Since ES2021, WeakRef lets you observe this without keeping an object alive Source: MDN: Memory management.
Check yourself: What do garbage collectors start from when finding reachable objects?
Roots: the global object, the current call stack, and similar.
Edge cases and trapsWhere people slip
This is V8 behavior, not the spec. Closures created in the same scope share one context object, so keepSmall keeps big alive because useBig captured it:
import { setFlagsFromString } from "node:v8";
import { runInNewContext } from "node:vm";
setFlagsFromString("--expose-gc");
const gc = runInNewContext("gc");
const tick = () => new Promise((r) => setTimeout(r, 0));
let keepSmall;
function setup() {
const big = { data: new Array(1e6).fill(0) };
const small = "ok";
const useBig = () => big.data.length;
keepSmall = () => small;
useBig();
return new WeakRef(big);
}
const ref = setup();
await tick(); gc();
console.log("big while keepSmall lives:", ref.deref() ? "alive" : "collected");
keepSmall = null;
await tick(); gc();
console.log("big after keepSmall = null:", ref.deref() ? "alive" : "collected");Output (verified)
big while keepSmall lives: alive big after keepSmall = null: collected
Check yourself: In V8, why can a closure that only uses `small` keep `big` alive?
Closures from the same scope share one context; another closure captured big into it.
In productionStaff-level answers
Find leaks with Memory panel heap snapshots: take one, repeat the action five times, take another, use the Comparison view, and search “Detached” Source: Chrome DevTools: Record heap snapshots. Prevent them with effect cleanups, AbortController signals for listeners, and WeakMap for caches keyed by objects (JS.06.02).
Check yourself: Which DevTools filter shows nodes removed from the page but still in memory?
Searching the heap snapshot for "Detached".
Finished the five steps? Mark it, and its cards join your reviews.
Last checked 2026-10-31. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.
Interview questions this answers
- How can closures cause memory leaks?