Skip to main content

What are WeakMap, WeakSet, and WeakRef?

WeakMap, WeakSet, and WeakRef hold weak references to objects, so the JavaScript garbage collector can automatically reclaim those objects when nothing else points to them.

Theory

TL;DR

  • Regular Map/Set keep objects alive as long as they exist; weak versions let the GC collect them freely
  • Analogy: a party guest list where names erase themselves when guests leave, no cleanup code needed
  • WeakMap stores key-value pairs (keys must be objects); WeakSet stores unique objects; WeakRef wraps one object with manual .deref() access
  • None of the three support iteration, .size, or .clear(), and that is by design
  • Use weak versions when object lifetime and data lifetime differ (DOM caches, event handler tracking)

Quick example

javascript
// Map holds obj in memory even after obj = null const strongMap = new Map(); let obj = { data: 'example' }; strongMap.set(obj, 'value'); obj = null; // Map still holds the original object - memory stays allocated // WeakMap releases it const weakMap = new WeakMap(); let obj2 = { data: 'example' }; weakMap.set(obj2, 'value'); obj2 = null; // GC can now reclaim obj2, and the WeakMap entry disappears with it

After obj2 = null, no strong references remain. The GC reclaims the object, and the WeakMap entry goes with it automatically.

Key difference

Regular Map and Set hold strong references. The GC sees "something points to this object" and leaves it alone. Weak versions tell the GC: "I know about this object, but don't keep it alive for me." When the object has no other strong references, GC collects it and removes the weak entry. This is what stops DOM caches and event handler registries from leaking memory over time.

WeakMap

WeakMap is a key-value store where keys must be objects. Values can be anything. The API is small on purpose: set, get, has, delete. No .size. No iteration. No .keys().

Why no iteration? To give you a list of keys, JavaScript would need to hold strong references to all of them. That defeats the whole point.

WeakSet

WeakSet stores unique objects with the same GC behavior. You get add, has, delete. That is the entire API. The typical use case is marking objects as "visited" or "processed" without holding them in memory indefinitely.

WeakRef

WeakRef wraps a single object and exposes it through .deref(). Unlike WeakMap, there is no automatic side-effect cleanup. It just gives you a reference that does not block GC. Once the object is collected, .deref() returns undefined. Check the return value every time before using it.

javascript
let target = { id: 42 }; const ref = new WeakRef(target); target = null; // some time passes... const obj = ref.deref(); if (obj) { console.log(obj.id); // safe } else { console.log('already collected'); }

When to use

  • Temporary DOM node caches where nodes get unmounted: WeakMap (node as key, computed data as value)
  • Tracking which objects have been processed without owning them: WeakSet
  • Optional caching where reuse is desirable but not required: WeakRef with .deref() check
  • Permanent data that must survive regardless: plain Map or Set
  • Primitive keys (strings, numbers): plain Map, since WeakMap only accepts objects

Comparison table

FeatureMap / SetWeakMap / WeakSet / WeakRef
Key / value typesAny (primitives + objects)Objects only
GC behaviorStrong ref, blocks GCWeak ref, allows GC
IterableYes (keys, values, entries)No
.sizeYesNo
.clear()YesNo
When to usePersistent data storageEphemeral tracking (DOM caches, request metadata)

How the engine handles this

V8 (Chrome and Node.js) uses mark-sweep GC. During the mark phase, it follows strong references. Objects with no incoming strong references get swept. WeakMap entries use an internal structure called "ephemeron": the entry survives only if its key survives the mark phase. WeakRef works the same way but requires a manual .deref() call. Node.js 14+ and Chrome 84+ support all three.

Common mistakes

1. Expecting .size on WeakMap

javascript
const wm = new WeakMap(); wm.set({}, 1); console.log(wm.size); // undefined - no error, just no property

There is no .size. Track counts with a separate Map if you need them.

2. Using a primitive as a WeakMap key

javascript
const wm = new WeakMap(); wm.set('key', 1); // TypeError: Invalid value used as weak map key

WeakMap only takes objects. Wrap the primitive in an object, or use a plain Map.

3. Treating WeakRef as guaranteed storage

javascript
const ref = new WeakRef({ data: 'important' }); // ... later, no strong reference kept ... ref.deref().data; // may throw - deref() can return undefined

Always pattern-match: const obj = ref.deref(); if (obj) { use(obj); }.

4. Adding primitives to WeakSet

javascript
const ws = new WeakSet(); ws.add(42); // TypeError: Invalid value used in weak set

WeakSet is for object identity tracking. Use a plain Set for primitives.

5. Trying to iterate WeakMap

javascript
const wm = new WeakMap(); for (const [k, v] of wm) { } // TypeError: wm is not iterable

Weak collections are intentionally non-iterable. Switch to Map if you need to loop.

Real-world usage

  • react-window uses WeakMap to cache virtualized row heights with DOM nodes as keys; when nodes unmount, cache entries clean up automatically
  • Node.js http module uses WeakMap for request metadata tracking without leaking on aborted requests
  • Lodash _.memoize supports WeakMap-based caching for object arguments
  • Preact Signals use WeakRef for detached observers so they do not block component cleanup

Follow-up questions

Q: What does let m = new WeakMap(); let o = {}; m.set(o, 1); o = null; console.log(m.has(o)); output?
A: false. After o = null, the variable holds null. So m.has(null) is false. The original object is now eligible for GC.

Q: Why can't you iterate a WeakMap?
A: Iteration requires holding strong references to all keys at once. That would prevent GC from collecting them, which is exactly what WeakMap is supposed to avoid.

Q: When would you pick WeakRef over WeakMap?
A: When you have one object and no associated value to store. WeakMap is for key-value pairs tied to an object's lifetime. WeakRef is for "I want to check if this object is still around."

Q: Can GC timing vary between Node.js and the browser?
A: Yes. Node.js V8 tends to be more aggressive with GC cycles. Browsers throttle GC for responsiveness. Do not rely on .deref() returning a value for any specific duration.

Q: (Senior) How would you build a leak-proof cache that also tracks entry count?
A: Use a WeakMap for the cache (objects released automatically) plus a plain Map for the count, incrementing on set and decrementing on known deletes. The WeakMap handles GC; the counter handles accounting. The trade-off: when GC collects entries you did not explicitly delete, the count drifts. That is accepted behavior for this pattern.

Examples

Basic: DOM node cache

javascript
const styleCache = new WeakMap(); function getComputedColor(node) { if (!styleCache.has(node)) { // getComputedStyle is an expensive DOM call - cache the result styleCache.set(node, window.getComputedStyle(node).color); } return styleCache.get(node); } const button = document.querySelector('#submit'); console.log(getComputedColor(button)); // 'rgb(0, 0, 0)' // When button is removed from the DOM and no code holds a reference to it, // GC collects the node and styleCache drops the entry automatically.

No cleanup code needed. Working with a virtualized list once, I swapped a plain Map cache for WeakMap in a row height calculator. Memory in Chrome DevTools dropped visibly after rapid row mounts and unmounts. The old Map had been accumulating every detached row node without releasing anything.

Intermediate: Tracking processed requests with WeakSet

javascript
const processedRequests = new WeakSet(); async function handleRequest(req) { if (processedRequests.has(req)) { return; // already handled, skip } processedRequests.add(req); const data = await fetch(req.url).then(r => r.json()); console.log('Processed:', data); // When req goes out of scope, WeakSet releases it automatically. // A plain Set would hold every request object for the server's entire lifetime. }

The key point: you never call processedRequests.delete(req). The GC handles it when the request object is no longer referenced anywhere else.

Advanced: Optional image cache with WeakRef

javascript
class ImageLoader { #cache = new Map(); // url -> WeakRef of the img element load(url) { const existing = this.#cache.get(url); if (existing) { const img = existing.deref(); if (img) { return img; // still alive, reuse it } this.#cache.delete(url); // stale entry, clean it up } const img = new Image(); img.src = url; this.#cache.set(url, new WeakRef(img)); return img; } } const loader = new ImageLoader(); const img1 = loader.load('https://example.com/photo.jpg'); // loads fresh const img2 = loader.load('https://example.com/photo.jpg'); // reuses img1 if still alive

When the browser needs memory, it can collect image elements. The .deref() check handles both cases without crashing.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?