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/Setkeep 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
WeakMapstores key-value pairs (keys must be objects);WeakSetstores unique objects;WeakRefwraps 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
// 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 itAfter 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.
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:
WeakRefwith.deref()check - Permanent data that must survive regardless: plain
MaporSet - Primitive keys (strings, numbers): plain
Map, since WeakMap only accepts objects
Comparison table
| Feature | Map / Set | WeakMap / WeakSet / WeakRef |
|---|---|---|
| Key / value types | Any (primitives + objects) | Objects only |
| GC behavior | Strong ref, blocks GC | Weak ref, allows GC |
| Iterable | Yes (keys, values, entries) | No |
.size | Yes | No |
.clear() | Yes | No |
| When to use | Persistent data storage | Ephemeral 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
const wm = new WeakMap();
wm.set({}, 1);
console.log(wm.size); // undefined - no error, just no propertyThere is no .size. Track counts with a separate Map if you need them.
2. Using a primitive as a WeakMap key
const wm = new WeakMap();
wm.set('key', 1); // TypeError: Invalid value used as weak map keyWeakMap only takes objects. Wrap the primitive in an object, or use a plain Map.
3. Treating WeakRef as guaranteed storage
const ref = new WeakRef({ data: 'important' });
// ... later, no strong reference kept ...
ref.deref().data; // may throw - deref() can return undefinedAlways pattern-match: const obj = ref.deref(); if (obj) { use(obj); }.
4. Adding primitives to WeakSet
const ws = new WeakSet();
ws.add(42); // TypeError: Invalid value used in weak setWeakSet is for object identity tracking. Use a plain Set for primitives.
5. Trying to iterate WeakMap
const wm = new WeakMap();
for (const [k, v] of wm) { } // TypeError: wm is not iterableWeak collections are intentionally non-iterable. Switch to Map if you need to loop.
Real-world usage
react-windowusesWeakMapto cache virtualized row heights with DOM nodes as keys; when nodes unmount, cache entries clean up automatically- Node.js
httpmodule usesWeakMapfor request metadata tracking without leaking on aborted requests - Lodash
_.memoizesupports WeakMap-based caching for object arguments - Preact Signals use
WeakReffor 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
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
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
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 aliveWhen the browser needs memory, it can collect image elements. The .deref() check handles both cases without crashing.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.