Understanding Garbage Collection in JavaScript
Garbage collection is an automatic memory management feature that helps ensure efficient memory usage by reclaiming memory occupied by objects that are no
Introduction to Garbage Collection
Garbage collection automatically manages memory in JavaScript. It frees the memory used by data that is no longer needed, so you almost never allocate or release memory by hand. This page explains the single idea the whole system rests on — reachability — then covers the mark-and-sweep algorithm, the leaks that still slip through, and how WeakMap/WeakSet help you avoid them.
Understanding this matters because "automatic" does not mean "leak-proof." The collector only removes what it can prove is unreachable; if your code keeps a hidden reference alive, the memory stays allocated for the life of the program.
Reachability: the core concept
The engine does not track whether an object is "in use" in a logical sense. It tracks whether the object is reachable — whether there is any chain of references that leads to it from a root.
Roots are values the engine always keeps:
- The currently executing function's local variables and parameters.
- Variables and functions on the current chain of nested calls.
- Global variables (properties of
globalThis/window).
Any object reachable by following references from a root — directly or through other reachable objects — is kept. Everything else is garbage.
Example: a reference keeps an object alive
Explanation: The object { name: "John" } was reachable through user. Copying the reference into admin creates a second path to it. Setting user = null removes one path, but admin still points at the object, so it stays reachable and is not collected.
Interlinked objects are still collected
A common myth is that objects which reference each other survive. They do not — what matters is reachability from a root, not whether objects point at one another.
Explanation: obj1 and obj2 form a cycle, but once both root variables are set to null there is no path from any root into the cycle. The whole island becomes unreachable and eligible for collection. This is why JavaScript engines use reachability rather than naive reference counting, which would leak on cycles.
How the collector works: mark-and-sweep
JavaScript engines reclaim memory with the mark-and-sweep algorithm. It reduces "this object is no longer needed" to the precise question "this object is no longer reachable."
- Mark. Starting from the roots, the collector visits every reachable object and marks it. It then follows their references, marks those objects, and so on until every reachable object is marked.
- Sweep. Every object that was not marked is unreachable. The memory it occupies is freed.
You cannot trigger this manually and should not try to — there is no standard gc() in the language. Real engines (like V8) refine the basic algorithm with optimizations such as generational collection (new objects die young, so check them more often) and incremental collection (split the work into chunks to avoid pauses). The reachability model you reasoned about above stays the same.
Common sources of memory leaks
A leak in JavaScript is simply an object that stays reachable even though your program is done with it. The collector is working correctly — it just cannot tell the reference is stale. Watch for these patterns.
Forgotten timers and intervals
A pending setInterval (or setTimeout) keeps its callback alive, and the callback keeps everything it closes over alive. If you never clearInterval, that memory is held for the life of the page.
Detached DOM nodes
If you remove an element from the page but keep a reference to it in a variable, the node — and its entire subtree — cannot be collected.
let detached = document.getElementById('list');
document.body.removeChild(detached);
// The node is gone from the page, but 'detached' still references it,
// so it stays in memory. Release it when done:
detached = null;Lingering event listeners
A listener bound to a DOM element keeps both the element and the handler (with everything it closes over) reachable. Remove listeners with removeEventListener once they are no longer needed:
<button id="myButton">Click me</button>
<script>
const button = document.getElementById('myButton');
function alertClick() {
alert("Button clicked!");
button.removeEventListener('click', alertClick); // free the listener after first use
}
button.addEventListener('click', alertClick);
</script>The button responds only to the first click: the handler removes itself with removeEventListener, releasing the reference so it can be garbage collected.
Global caches that only grow
A cache stored in a module-level or global object keeps every entry reachable forever unless you explicitly evict old ones. An unbounded Map used as a cache is a classic slow leak.
Closures that capture more than you think
A closure keeps alive every variable in the scopes it references — even ones it never actually uses. Returning a small inner function from a function with large locals can pin those locals in memory. Keep captured scope minimal, and see variable scope for how closures retain their environment.
How WeakMap and WeakSet help
Map and Set hold strong references to their keys/values, so anything stored in them stays reachable. WeakMap and WeakSet hold their keys weakly: if the only remaining reference to an object is the one inside a WeakMap, the object can still be collected, and the entry disappears with it.
This makes WeakMap ideal for associating extra data with objects (caches, metadata, DOM-node bookkeeping) without forcing those objects to live forever. Because entries can vanish at any time, WeakMap/WeakSet are deliberately not iterable and have no size.
Best practices
- Prefer local variables; they go out of scope automatically and become collectible when the function returns.
- Limit global variables — they live as long as the application does. Use modules and block scope (
let/const). - Clear timers (
clearInterval/clearTimeout) and remove event listeners withremoveEventListeneronce they are no longer needed. - Null out references to detached DOM nodes and other large objects you are finished with.
- Use
WeakMap/WeakSetfor object-keyed caches and metadata so entries don't outlive their keys.
Conclusion
Garbage collection in JavaScript is built entirely on reachability: the engine keeps any object it can reach from a root and frees the rest using mark-and-sweep, which handles even reference cycles. "Automatic" still leaves you responsible for not holding stale references — forgotten timers, detached DOM nodes, ever-growing caches, and over-capturing closures are the usual culprits. Reach for WeakMap and WeakSet when you want to associate data with objects without keeping them alive.