Q116: Garbage Collection and Memory Leak Patterns
What Interviewers Want To Evaluate
This question checks whether you understand how JavaScript memory is reclaimed and why frontend apps still leak memory. Interviewers want reachability, roots, retaining paths, closures, detached DOM nodes, listeners, timers, caches, heap snapshots, and cleanup discipline.
Senior answers should connect memory behavior to production debugging rather than only describing garbage collection theory.
Short Interview Answer
JavaScript engines reclaim memory through garbage collection. The key idea is reachability: values reachable from roots such as the call stack, globals, closures, DOM references, and active tasks are kept alive. Memory leaks happen when an app accidentally keeps references to data it no longer needs. Common frontend leaks include unremoved event listeners, long-lived timers, retained closures, unbounded caches, detached DOM nodes, and stale subscriptions.
Detailed Interview Answer
JavaScript manages memory automatically, but automatic does not mean leak-proof.
The engine can collect objects only when they are no longer reachable.
root references -> reachable objects -> retained memory
If an object is still reachable from somewhere, the garbage collector must assume it may be used again.
Reachability
Reachability is the practical interview model.
Common roots include:
global variables
current call stack
closures
DOM tree references
active event listeners
timers and intervals
pending promises and callbacks
module-level caches
An object is collectible only after no retaining path keeps it reachable.
Simple Retention Example
const cache = [];
function remember(value) {
cache.push(value);
}
Every value pushed into cache remains reachable as long as cache remains reachable.
This may be intended or it may become an unbounded memory leak.
Closure Leaks
Closures can retain variables from their lexical environment.
function createHandler(largeData) {
return function handleClick() {
console.log(largeData.id);
};
}
If the handler stays registered, largeData can remain alive even after the UI that needed it is gone.
Event Listener Leaks
Event listeners often leak when added to long-lived objects and not removed.
function mountPanel(panel) {
function onResize() {
panel.recalculate();
}
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
};
}
The cleanup function is essential because window outlives most UI panels.
Timer and Interval Leaks
Intervals continue running until cleared.
const intervalId = setInterval(refresh, 5000);
clearInterval(intervalId);
If interval callbacks capture component state or DOM nodes, the interval can retain them indefinitely.
Detached DOM Nodes
A detached DOM node is removed from the document but still referenced by JavaScript.
const removed = document.querySelector(".modal");
removed.remove();
window.lastModal = removed;
The node is gone visually but not collectible because window.lastModal still references it.
Unbounded Caches
Caches need limits or invalidation.
const responseCache = new Map();
function store(key, value) {
responseCache.set(key, value);
}
This cache grows forever unless entries are removed, evicted, or bounded.
WeakMap helps only when object identity is the key and the key lifetime should control the entry lifetime.
Pending Async Work
Pending promises, callbacks, and request handlers may retain captured state.
This matters when a user navigates away, changes filters, or starts a newer request.
Cancellation and stale-result guards reduce retained work and prevent obsolete UI updates.
Garbage Collection Is Non-Deterministic
You cannot rely on exact collection timing.
The engine decides when to run garbage collection based on memory pressure, allocation behavior, and implementation details.
Good application code releases references and unregisters work. It does not depend on immediate collection.
Heap Snapshot Mental Model
When debugging memory, inspect retaining paths.
The retaining path explains why an object is still reachable.
Window
-> event listener
-> callback closure
-> old component state
-> large data array
This is often more useful than staring at total heap size.
React Connection
In React, common cleanup points include effect cleanup functions.
useEffect(() => {
const controller = new AbortController();
loadData(controller.signal);
return () => {
controller.abort();
};
}, []);
Cleanup prevents obsolete async work and subscriptions from living longer than needed.
Common Mistakes
A common mistake is assuming memory leaks require manual allocation. In JavaScript, leaks are usually accidental references.
Another mistake is clearing UI state but forgetting module-level caches, event listeners, or intervals.
Senior Trade-Offs
Some memory retention is intentional. Caches, virtualized lists, undo stacks, and preloaded routes all retain data for speed or UX.
The senior question is whether retention is bounded, observable, and tied to a clear lifecycle.
Code or Design Example
function createBoundedCache(limit) {
const cache = new Map();
return {
get(key) {
return cache.get(key);
},
set(key, value) {
if (cache.size >= limit) {
const oldestKey = cache.keys().next().value;
cache.delete(oldestKey);
}
cache.set(key, value);
},
};
}
This keeps retention explicit and bounded.
Debugging Workflow
When investigating leaks:
reproduce with a repeated workflow
record heap snapshots before and after
force garbage collection in dev tools if available
compare retained objects
inspect retaining paths
check listeners, timers, closures, caches, detached nodes
add cleanup and verify memory stabilizes
Interview Framing
Start with reachability. Then list practical leak patterns and explain how you would use heap snapshots to find retaining paths.
Revision Notes
Garbage collection reclaims unreachable objects. Leaks happen when obsolete objects remain reachable through accidental references.
Key Takeaways
Memory safety in JavaScript is about lifecycle ownership. Release listeners, timers, subscriptions, stale async work, and unbounded caches.
Follow-Up Questions
- What does reachability mean?
- What are garbage collection roots?
- How can closures retain memory?
- Why do event listeners leak?
- What is a detached DOM node?
- Why are unbounded caches risky?
- Is garbage collection deterministic?
- What is a retaining path?
- How do you debug a memory leak?
- When is memory retention intentional?
How I Would Answer This In A Real Interview
I would explain reachability first, then give concrete frontend leak patterns: listeners, intervals, closures, caches, detached DOM nodes, and pending async work. I would finish with heap snapshot retaining paths and cleanup verification.