JavaScript uses automatic garbage collection — the engine reclaims memory when objects are no longer reachable. Memory leaks occur when references are unintentionally retained, preventing collection.
Explanation
JavaScript engines (V8, SpiderMonkey) use a mark-and-sweep garbage collector: starting from GC roots (global scope, call stack), the collector marks all reachable objects and sweeps the rest. Memory is allocated on the heap for objects, arrays, closures, and strings, and on the stack for primitives in function scope. Leaks occur when references are unintentionally kept alive — common sources: event listeners not removed when elements are detached from the DOM, closures capturing large objects that outlive their usefulness, global variable accretion, and detached DOM nodes held in JS variables. WeakMap and WeakRef allow holding references that do not prevent GC. Node.js processes have a default heap limit (~1.5 GB on 64-bit) configurable with --max-old-space-size. The Chrome DevTools Memory panel provides heap snapshots, allocation timelines, and retained-size analysis to identify leak sources. In long-lived SPAs and Node.js servers, undetected leaks cause gradual memory growth until OOM.
Watch Out
⚠ WeakMap and WeakSet hold weak references and do not prevent GC — use them for metadata keyed by objects (e.g. private data for DOM nodes) where you do not want to control the object's lifetime.
Common Misconception
✗ Setting a variable to null does not immediately free memory — it removes the reference, making the object eligible for GC, but the collector decides when to actually reclaim the memory.
Why It Matters
Memory leaks in SPAs and long-running Node.js servers cause gradual performance degradation and eventual crashes — understanding retention patterns is necessary for stable production services.
Common Mistakes
Adding event listeners without removing them on cleanup — each listener holds a closure reference, keeping associated objects alive indefinitely.
Storing large objects in module-level or global variables — they live for the process lifetime and are never collected.
Accumulating items in a cache Map without a size limit or TTL — unbounded Maps are the most common Node.js memory leak.
Holding references to detached DOM nodes in JavaScript arrays or Maps — the nodes cannot be GC'd even though they are not in the document.
Code Examples
💡 Note
The bad example adds a new scroll listener on every call with no way to remove it — each listener retains a reference to globalCache via its closure, preventing GC. The AbortController pattern cleanly removes all listeners with a single abort() call.
✗ Vulnerable
// Listener leak — added on every render, never removed:
function attachHandler() {
document.addEventListener('scroll', () => {
processLargeData(globalCache); // holds reference to cache
});
// No removeEventListener — each call adds another permanent listener
}
✓ Fixed
// Cleanup with AbortController:
function attachHandler() {
const controller = new AbortController();
document.addEventListener('scroll', () => {
processLargeData(globalCache);
}, { signal: controller.signal });
return () => controller.abort(); // call this on unmount/cleanup
}