JavaScript WeakMap and WeakSet Explained
What WeakMap and WeakSet are, how their garbage collection semantics differ from Map and Set, and where they actually pay off in real code.
What you'll learn
- ✓How WeakMap and WeakSet differ from Map and Set
- ✓What "weak reference" actually means here
- ✓Use cases like private data and metadata
- ✓Memory leak patterns they prevent
- ✓Limitations and how to work around them
Prerequisites
- •Comfortable with JavaScript objects
Map and Set keep their keys alive forever, which is fine until you start using them for associating extra data with objects whose lifetime you do not control. WeakMap and WeakSet solve that specific problem: they hold references that the garbage collector is allowed to ignore. Used in the right places, they prevent leaks without requiring manual cleanup.
What and why
A WeakMap is a key-value collection where the keys must be objects (or symbols, in modern engines) and the references to those keys are “weak.” If no other code holds a reference to a key, the garbage collector is free to remove that key (and its associated value) from the map. You never see it disappear; it just stops being there.
A WeakSet is the analogous structure for unique objects without values.
The reason this matters: if you use a regular Map keyed by DOM elements to store per-element data, those elements can never be garbage collected, because the map holds a strong reference. Replace it with a WeakMap and the cleanup happens automatically when the element is removed from the DOM.
Mental model
A Map is like a parking lot where every parked car is held forever. A WeakMap is more like a “may park here” sign: if the car drives away (no other code references it), its spot is reclaimed.
Map:
map -----strong-----> key (kept alive)
|
+--> value
WeakMap:
weakmap --weak--> key (collectible)
|
+--> value (gone when key is collected)
if (no other strong ref to key) -> GC removes both Hands-on example
Associating private data with an object without polluting it:
const _internal = new WeakMap();
class Counter {
constructor() {
_internal.set(this, { count: 0 });
}
inc() {
const state = _internal.get(this);
state.count++;
return state.count;
}
}
const c = new Counter();
c.inc(); c.inc(); // 1, 2
When the Counter instance is no longer referenced, both the instance and its entry in _internal are eligible for collection. No leak.
Caching expensive computations per object:
const cache = new WeakMap();
function expensive(node) {
if (cache.has(node)) return cache.get(node);
const result = computeFrom(node);
cache.set(node, result);
return result;
}
If node is a DOM element that gets detached, the cache entry vanishes too.
Tracking visited objects in graph traversal:
function deepClone(obj, seen = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (seen.has(obj)) return seen.get(obj); // handle cycles
const copy = Array.isArray(obj) ? [] : {};
seen.set(obj, copy);
for (const key of Object.keys(obj)) copy[key] = deepClone(obj[key], seen);
return copy;
}
The seen WeakMap lets the function detect cycles without keeping every visited node alive after the call returns.
Limitations
WeakMap has no size, no iteration, no forEach, no keys(). The reason: those operations would require deterministic visibility into what is currently alive, which contradicts the whole point of being weak. You cannot ask “how many entries are there” because the answer would change every time the garbage collector ran.
If you need iteration, you need a regular Map, possibly with manual cleanup hooks. There is a FinalizationRegistry for advanced cases, but it has its own warnings — finalizers run on a “best effort” basis and should not hold critical logic.
Common pitfalls
- Trying to use primitives as keys. Strings, numbers, and booleans throw a
TypeError. Only objects (and modern symbols) are allowed. - Holding the key strongly elsewhere. If you store the same object in a regular array or
Map, the weak reference does not help — the strong reference dominates. - Relying on garbage collection timing. You cannot test that a
WeakMapentry has been collected; the GC has no public hook for it. - Using
WeakMapto store global state. It only makes sense when the keys have a meaningful lifetime; if the key is a singleton, a regular property is simpler.
Best practices
- Use
WeakMapfor associating data with objects you do not own (DOM nodes, third-party objects, library inputs). - Use
WeakSetfor “have I seen this object?” checks where you want the set to drop entries automatically. - Document the weakness. Future readers may not realize an absent entry just means the key was collected.
- For cycle-detection inside short-lived recursive functions, a plain
Setis fine.WeakSetbecomes useful when the recursion outlives the immediate call.
FAQ
Why can WeakMap not be iterated? Because iteration order would expose garbage collection timing, which is intentionally non-deterministic.
Are WeakMap operations slower than Map? Roughly similar in practice. The difference is the GC interaction, not per-operation speed.
Can a value in a WeakMap hold a strong reference to its key? No. The runtime ensures the key-to-value relationship does not create a cycle that keeps the key alive.
When should I use FinalizationRegistry instead? When you need to perform cleanup (closing a file handle, removing a listener) after an object is collected. Use it sparingly; finalizers are not guaranteed to run.
Related articles
- Node.js Debugging Node.js Memory Leaks
Find and fix memory leaks in Node.js using heap snapshots, sampling, and a few reliable patterns to avoid leaks.
- JavaScript The JavaScript Event Loop, Explained Clearly
A deep but practical look at the JavaScript event loop: call stack, microtasks, macrotasks, and how async code actually runs in the browser and Node.
- JavaScript JavaScript Generators and Iterators
A practical guide to JavaScript iterators and generator functions: the protocols, lazy sequences, async generators, and where they shine in real code.
- JavaScript JavaScript Modules: ESM vs CommonJS
A practical comparison of ES Modules and CommonJS: how they load, how they differ, interop strategies, and how to choose for a new project.