WeakMap and WeakRef in JavaScript, Explained
A WeakMap holds object keys without blocking garbage collection, unlike a regular Map. How WeakMap and WeakRef work and when to reach for them.
A WeakMap is a JavaScript collection that maps objects to values, just like a Map — except its keys don’t prevent those objects from being garbage collected. When nothing else in the program references a key, the engine is free to reclaim it, and the WeakMap entry disappears with it. That one property makes WeakMap the right tool for a specific job: attaching metadata to objects you don’t own the lifetime of.
Why a regular Map can leak memory
A Map holds a strong reference to every key you insert. Consider caching some computed data per DOM node:
const cache = new Map();
function getData(el) {
if (!cache.has(el)) cache.set(el, computeExpensiveThing(el));
return cache.get(el);
}
If el is later removed from the DOM and nothing else points to it, the browser still can’t garbage-collect it — the Map is holding a reference. Every element you’ve ever passed to getData stays in memory until you manually delete it from the cache. In a long-running single-page app, this is a slow, easy-to-miss memory leak, the same category of bug covered in what a race condition is for timing bugs — subtle and easy to ship without noticing.
How WeakMap fixes it
Swap Map for WeakMap and the problem disappears:
const cache = new WeakMap();
function getData(el) {
if (!cache.has(el)) cache.set(el, computeExpensiveThing(el));
return cache.get(el);
}
Now the cache entry for el doesn’t keep el alive. Once the element is removed from the DOM and dropped elsewhere, the garbage collector can reclaim it, and its WeakMap entry is cleaned up automatically — no manual bookkeeping, no leak. This is a form of automatic resource management that pairs well with the discipline covered in memoization: caching results without holding onto data the rest of the program has moved on from.
The constraints that come with it
WeakMap’s memory behavior comes at the cost of introspection. The engine needs to be able to garbage-collect entries silently and unpredictably, so it can’t let your code observe the collection’s exact contents:
- Keys must be objects (or, in modern engines, registered symbols) — you can’t use a string, number, or boolean as a WeakMap key.
- No iteration. There’s no
.keys(),.values(),.entries(), orfor...ofsupport, and no.sizeproperty. You can only look up a key you already have a reference to. - Not clearable in bulk beyond
.get,.set,.has,.delete. There’s no.clear()method either.
These aren’t oversights — they’re what makes the “weak” behavior safe. If you could iterate a WeakMap, you’d be holding a strong reference to every key just by inspecting the collection, defeating the entire point.
WeakMap vs Map
Map | WeakMap | |
|---|---|---|
| Key types | Any value | Objects (and registered symbols) only |
| Keeps keys alive | Yes | No |
| Iterable | Yes (for...of, .keys(), etc.) | No |
Has .size | Yes | No |
| Typical use | General-purpose key-value storage | Private/auxiliary data tied to an object’s lifetime |
The same distinction, and the same tradeoff of ergonomics for automatic cleanup, applies to Set vs WeakSet — see Map vs Set in JavaScript for the non-weak baseline this builds on.
Where WeakMap shows up in practice
- Private class fields (pre-2022 pattern). Before native
#privatefields, libraries used a module-scoped WeakMap keyed bythisto store private state without exposing it on the instance. - Caching derived data per object. Memoizing a computation keyed by a DOM node, a class instance, or a parsed AST node, as in the example above.
- Tracking metadata without mutating the original object. Frameworks use WeakMaps to associate internal bookkeeping (render state, listeners, observers) with user-supplied objects they can’t add properties to directly.
- Implementing
Proxytraps that need out-of-band storage. Combined with JavaScript Proxy objects, a WeakMap is a common way to keep a proxy’s internal state separate from the object it wraps.
WeakRef: the single-value version
WeakRef is a related but distinct primitive: instead of a weak entry in a collection, it’s a weak reference to a single object. You create one with new WeakRef(obj), and later call .deref() to get the object back — or undefined if it’s already been collected.
const ref = new WeakRef(someObject);
// later, possibly after someObject has no other references:
const obj = ref.deref();
if (obj) {
// still alive — use it
}
WeakRef is a lower-level building block than WeakMap, and the language spec is explicit that garbage collection timing is not observable or guaranteed — you should never rely on a WeakRef being cleared at any particular moment. It’s paired with FinalizationRegistry, which lets you register a callback to run (eventually, with no timing guarantee) after an object is collected, useful for cleaning up external resources like file handles or WebAssembly memory tied to a JS object’s lifetime.
Both are specialized tools. If you’re reaching for WeakRef directly in application code, it’s worth double-checking whether a WeakMap — which handles the common “attach data to an object” case without exposing GC timing at all — would do the job more simply.
When not to use them
If you need to enumerate what’s in your collection, print its size, or guarantee entries stick around for a fixed duration, use a regular Map with explicit cleanup (a TTL, an LRU eviction policy, or a manual .delete() on teardown) instead. WeakMap trades observability for automatic cleanup; when you need the former, it’s the wrong tool.
The takeaway
A WeakMap maps objects to values without keeping those objects alive, which makes it the right default for attaching cache data or private state to objects you don’t control the lifetime of. The cost is no iteration and no size — you can only look up keys you already hold. WeakRef and FinalizationRegistry extend the same weak-reference idea to single values and cleanup callbacks, but they’re rarely needed outside library and framework code. For everyday key-value storage, reach for a regular Map first and only switch when memory-safety around object lifetimes is the actual problem you’re solving.
Tagged
Keep reading
Takina · · 5 min read Promise.all() vs allSettled() vs race() Compared
Promise.all() fails fast, allSettled() waits for every result, and race() returns whichever promise finishes first — how to choose correctly.
Takina · · 4 min read JavaScript Spread vs Rest Operators, Explained
The spread operator (...) expands an iterable into individual elements; the rest operator collects elements back into an array. Same syntax, opposite jobs.
Takina · · 4 min read ResizeObserver API Explained: Watching Element Size
The ResizeObserver API lets JavaScript watch an element's box size and react without polling or resize-event hacks. How it works and when to use it.