WeakMap & WeakSet
WeakMap and WeakSet are specialized collections that hold weak references to the objects they store. Unlike Map and Set, they don’t prevent their keys (or members) from being garbage-collected: once nothing else in your program references an object, the engine is free to reclaim it and quietly drop the corresponding entry. This makes them ideal for associating private data or cached metadata with objects without risking memory leaks.
Why weak references matter
A normal Map keeps a strong reference to every key. As long as the Map exists, its keys can never be garbage-collected — even if the rest of your code has forgotten about them. That’s perfect when you want stable, iterable storage, but it’s a hazard when you’re attaching data to objects whose lifetime you don’t control (DOM nodes, request objects, user-supplied instances).
WeakMap solves this by not counting toward an object’s reachability. If the only reference to an object is its presence as a WeakMap key, that object becomes eligible for collection, and the engine removes the entry automatically.
let user = { name: "Ada" };
const lastSeen = new WeakMap();
lastSeen.set(user, Date.now());
console.log(lastSeen.has(user)); // true
user = null; // the object is now unreachable elsewhere
// At some later point the GC may reclaim it,
// and the WeakMap entry disappears with it — no manual cleanup.
Output:
true
Object-only keys
Because weak referencing only makes sense for things the garbage collector tracks, WeakMap keys and WeakSet members must be objects (or non-registered symbols, since ES2023). Primitives like strings and numbers are interned or value-typed and would throw.
const wm = new WeakMap();
wm.set({}, "ok"); // fine — object key
wm.set([], "also ok"); // fine — arrays are objects
try {
wm.set("string-key", 1); // TypeError: Invalid value used as weak map key
} catch (err) {
console.log(err.name);
}
Output:
TypeError
Not iterable, not measurable
You cannot loop over a WeakMap or WeakSet, read its size, or clear it wholesale. There’s a good reason: entries can vanish at unpredictable times during garbage collection, so exposing iteration would make program behavior non-deterministic. The API is deliberately tiny.
| Capability | WeakMap | WeakSet | Map / Set |
|---|---|---|---|
| Allowed keys/values | objects (+ symbols) | objects (+ symbols) | any value |
size property | no | no | yes |
| Iterable / spreadable | no | no | yes |
forEach | no | no | yes |
clear() | no | no | yes |
| Holds entries strongly | no | no | yes |
The available methods are get, set, has, and delete for WeakMap; and add, has, and delete for WeakSet.
Because you can’t enumerate them, treat WeakMap/WeakSet as write-and-look-up stores keyed by an object you already hold — never as a place to “list everything you’ve stored.”
Use case: truly private data
A classic pattern is storing per-instance private state in a module-scoped WeakMap. The data is keyed by the instance, inaccessible from outside the module, and cleaned up automatically when the instance is collected.
const privates = new WeakMap();
class BankAccount {
constructor(balance) {
privates.set(this, { balance });
}
deposit(amount) {
privates.get(this).balance += amount;
return this;
}
get balance() {
return privates.get(this).balance;
}
}
const acct = new BankAccount(100);
acct.deposit(50);
console.log(acct.balance);
Output:
150
Modern code often reaches for native
#privateclass fields instead, but aWeakMapstill shines when you need to attach private data to objects you didn’t create — like third-party instances or DOM elements.
Use case: leak-free caches and metadata
WeakMap is the natural backing store for a memoization cache or for tagging objects with computed metadata. When the underlying object goes away, its cached result goes with it, so the cache never grows unbounded.
const sizeCache = new WeakMap();
function getNodeArea(el) {
if (sizeCache.has(el)) {
return sizeCache.get(el);
}
const rect = el.getBoundingClientRect();
const area = rect.width * rect.height;
sizeCache.set(el, area);
return area;
}
WeakSet: tracking membership
WeakSet stores a collection of unique objects and answers a single question: “have I seen this object?” It’s perfect for marking objects as visited or processed without keeping them alive.
const processed = new WeakSet();
function handle(task) {
if (processed.has(task)) {
return "skipped (already done)";
}
processed.add(task);
return "processed";
}
const job = { id: 1 };
console.log(handle(job));
console.log(handle(job));
Output:
processed
skipped (already done)
Choosing between weak and strong collections
Reach for Map/Set when you need to iterate, count entries, use primitive keys, or deliberately keep values alive. Reach for WeakMap/WeakSet when you’re decorating externally-owned objects with side data and want the cleanup to be automatic. If you ever find yourself wanting to list a WeakMap’s contents, that’s a strong sign you actually wanted a Map.
Best Practices
- Use
WeakMapto attach private state or caches to objects whose lifetime you don’t own, so entries clean up themselves. - Remember keys must be objects (or non-registered symbols) — guard against primitive keys if input is untrusted.
- Don’t rely on garbage collection timing; entries may persist for a while. Weak collections manage memory, not deterministic deletion.
- Prefer native
#privatefields for first-party classes; chooseWeakMapfor third-party or DOM objects. - Never try to iterate or read
size— design around lookups by an object you already hold. - For deterministic teardown that must run when an object is collected, look at
FinalizationRegistryandWeakRef, which complement these collections.