What this usually means
WeakMap and WeakSet hold weak references to their keys, meaning if the key is only referenced inside the WeakMap, it can be GC'd. However, a common mistake is to use a primitive value or a non-object as a key (which throws TypeError) or to inadvertently keep a strong reference to the key elsewhere. More subtly, if the WeakMap stores values that reference the key, it creates a cycle that may prevent GC depending on the engine. Also, many developers use WeakMap as a cache for computed data, but if the value strongly references the key, the key becomes strongly reachable and the WeakMap entry never gets collected.
The first ten minutes — establish facts before touching code.
- 1Take a heap snapshot before and after a typical user session (e.g., navigate 10 pages). Compare retained sizes of WeakMap/WeakSet entries.
- 2In Chrome DevTools, use the 'Retainers' pane to trace why a supposedly dead object is still held by a WeakMap.
- 3Add a debug logging statement when adding entries to the WeakMap: console.log('Adding key', key, 'with value', value). Check for duplicates or stale keys.
- 4Run the app with --trace-gc in Node.js and look for 'weak' references that are not being collected.
- 5Use `process.memoryUsage()` in Node to monitor heapUsed over time and correlate with WeakMap usage.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchChrome DevTools Memory tab: heap snapshots, allocation timelines, and the 'Detached DOM Tree' report.
- searchNode.js heap snapshot (using --inspect) or the `heapdump` module: look for WeakMap retention paths.
- searchThe codebase search for `new WeakMap()` and `new WeakSet()` — inspect all usages for potential leaks.
- searchThe `finalizationRegistry` if used, check if cleanup is happening as expected.
- searchApplication profiling logs: if you log WeakMap size, look for growth without reset.
- searchThe GC trace output: filter for 'weak' and 'unreachable' to spot patterns.
Practical causes, not theory. These are the things you will actually find.
- warningUsing a primitive value as a WeakSet key — throws TypeError, but if caught silently, the set never stores the key.
- warningHolding a strong reference to the key object elsewhere (e.g., in an array or closure) so the WeakMap entry never becomes collectable.
- warningWeakMap values referencing the key object, creating a cycle that prevents GC (value → key → WeakMap entry).
- warningUsing WeakMap as a cache without eviction: values hold references to other large objects that accumulate.
- warningMisunderstanding WeakSet: thinking it holds weak references to values, but it actually holds weak references to the objects added; if you add a primitive, it fails silently.
- warningForgetting to call `delete` on WeakMap when the key is no longer needed (e.g., on component unmount in React).
Concrete fix directions. Pick the one that matches your root cause.
- buildEnsure all keys are objects and that you don't hold any other strong reference to them. Use `null` out references when done.
- buildFor caches, use a regular Map with explicit eviction or a LRU cache library instead of WeakMap.
- buildIf using WeakMap to associate data with DOM elements, remove the element from the DOM and ensure no JavaScript variable refers to it.
- buildUse `FinalizationRegistry` to clean up after keys are garbage collected, but be aware of its limitations.
- buildWrap WeakMap access in a pattern that checks for key existence and deletes it after use (like a weak reference-based memoization).
A fix you cannot prove is a guess. Close the loop.
- verifiedTake a heap snapshot after the fix, perform the same user actions, and verify the WeakMap entries are collected.
- verifiedUse `setInterval` to log the size of a WeakMap (though you can't get size directly, you can count via iteration, but that's not possible with WeakMap). Instead, monitor the number of objects that should be keys.
- verifiedAdd a finalization callback via `FinalizationRegistry` to log when a key is collected.
- verifiedRun the app under load and monitor `heapUsed` for a steady state (no growth).
- verifiedCheck that detached DOM nodes no longer have WeakMap references in DevTools.
Things that make this bug worse or harder to find.
- warningDon't assume WeakMap automatically collects entries as soon as the key is not needed; it only collects when the key is garbage collected, which may be delayed.
- warningDon't use WeakMap if you need to iterate over entries or know the size — you can't.
- warningDon't rely on WeakMap for caching computed values that are derived from the key — those values may prevent GC if they reference the key.
- warningDon't confuse WeakSet with a set of weak references to values; it's a set of objects that are held weakly, meaning if an object is only in the WeakSet, it can be GC'd.
- warningAvoid using `constructor` or other internal objects as keys without understanding their lifecycle.
The Phantom Tab Growth: A WeakMap Cache Gone Wrong
Timeline
- 09:15Alert: memory usage on user dashboard page grows 200MB over 20 minutes of use.
- 09:30Take heap snapshot: 15,000 detached DOM nodes, all referenced by WeakMap in analytics library.
- 09:45Identify WeakMap key: each analytics event is an object that is stored in an array (strong ref) and also used as WeakMap key.
- 10:00Realize the analytics library uses WeakMap to store metadata per event, but events are kept in an array for batching.
- 10:15Fix: change WeakMap to regular Map and clear it after batch flush. Also null out event objects after sending.
- 10:30Deploy fix, monitor memory: heap stabilizes at 50MB.
We saw memory alerts on our dashboard after users spent more than 10 minutes. Heap snapshots showed thousands of detached DOM nodes. We traced each node to a WeakMap entry in our analytics library. The library used WeakMap to associate metadata with each analytics event object. But the event objects were also stored in a queue array for batching — a strong reference. So the WeakMap keys were never garbage collected, and the associated metadata (including DOM references) caused detached nodes.
At first I thought WeakMap would automatically clean up, but the key objects were still alive because of the array. The values in the WeakMap contained references to DOM elements that had been removed, so those elements became detached but still reachable through the WeakMap. That was the leak.
We replaced the WeakMap with a regular Map and cleared it after the batch was sent. We also set the event objects to null after sending. Memory usage dropped immediately. The lesson: WeakMap doesn't prevent leaks if the key is still strongly referenced elsewhere. Also, don't use WeakMap as a cache without understanding the lifecycle of keys and values.
Root cause
Analytics library used WeakMap keyed by event objects that were also strongly referenced in a queue array, preventing GC of both keys and their associated DOM references.
The fix
Replaced WeakMap with regular Map that was cleared after batch processing, and nullified event objects after sending.
The lesson
WeakMap does not protect against memory leaks if keys are held strongly elsewhere. Always consider the full reference graph.
WeakMap holds a weak reference to its keys, meaning if no other strong reference to the key exists, the key can be garbage collected, and the entry is removed. However, the values are held strongly. So if the value holds a reference back to the key, that creates a strong reference cycle that can prevent GC in some engines (though modern V8 can handle cycles). The key insight: the weakness is only on the key side, not the value side.
Similarly, WeakSet holds weak references to its objects. If an object is only in a WeakSet, it can be GC'd. But if you add a primitive, it's ignored (or throws in strict mode). Many developers mistakenly think WeakSet holds weak references to values (like a weak version of Set), but that's not the case.
The most common anti-pattern is using WeakMap as a cache for computed properties. For example, associating a complex object with a computed result. If the computed result depends on the key and references it, you create a cycle. Also, if the key is reused (e.g., same object appears again), the WeakMap may still hold the old entry if the key hasn't been collected.
Another pattern is using WeakSet to track 'active' objects, but forgetting to remove them when they become inactive. Since WeakSet doesn't provide a way to check membership without keeping a reference, you might end up with lingering entries. In practice, WeakSet is rarely useful; most use cases are better served by a regular Set with explicit cleanup.
Open DevTools > Memory > Take heap snapshot. Filter by 'detached' to find detached DOM trees. Expand a detached node and look at 'Retainers' — you'll see the WeakMap that references it. The path will show the key object that is still alive. Use the 'All objects' view to find all instances of the WeakMap and inspect their contents (though you can't see keys directly, you can see the values).
Another technique: use 'Allocation instrumentation on timeline' to see where WeakMap entries are being created and whether they are ever released. Look for growing number of entries without corresponding decrease.
In Node.js, WeakMap is used internally by many libraries (e.g., for storing metadata on objects). To debug, use `--inspect` and connect Chrome DevTools. Or use the `heapdump` module to take snapshots at intervals. Look for 'weakmap' in the snapshot's strings. You can also use `process.memoryUsage()` to monitor heap growth.
If you suspect a WeakMap leak, you can try to force GC with `global.gc()` (use `--expose-gc` flag) and check if heap size drops. If not, there are strong references somewhere.
Use WeakMap when you need to associate private data with an object without preventing its garbage collection. The classic example is storing metadata for DOM elements. If the element is removed from the DOM and no JS variable references it, the WeakMap entry is automatically cleaned. Use it for caching only if the cached value does not reference the key.
Avoid WeakMap when you need to iterate over entries, check size, or when the keys are primitives. Also avoid if you need to clear all entries at once (there's no `clear` method). In many cases, a regular Map with explicit removal is simpler and safer.
Frequently asked questions
Can WeakMap cause a memory leak?
Yes, if the key object is still strongly referenced elsewhere, the WeakMap entry will never be garbage collected. Also, if the value holds a strong reference to the key, it can create a cycle that prevents GC in some engines. WeakMap is not a silver bullet for memory management.
How do I see the keys of a WeakMap in a heap snapshot?
You cannot see WeakMap keys directly in a heap snapshot because they are held weakly. However, you can inspect the values and trace back to the keys via retainers. Alternatively, you can add a debug version that uses a Map and logs keys.
Is WeakSet useful for preventing memory leaks?
Rarely. WeakSet is typically used to mark objects without preventing their GC. But if you need to check membership, you must keep a reference to the object, which defeats the purpose. In practice, most use cases are better served by a Set with explicit removal.
What is the difference between WeakMap and FinalizationRegistry?
WeakMap automatically removes the entry when its key is GC'd, but you don't get a notification. FinalizationRegistry lets you register a callback that runs after an object is GC'd. They can be combined, but FinalizationRegistry is more useful for cleanup actions beyond memory.
How do I prevent memory leaks when using WeakMap with React components?
If you use WeakMap to associate data with a component instance, ensure you don't keep a reference to the instance elsewhere. In React, the component's fiber is an internal object; using it as a key is risky. Better to use a ref or a unique ID in a regular Map and clean up in useEffect return.