WeakSet and WeakMap
WeakSet and WeakMap are siblings of Set and Map with one critical difference: they hold weak references to their keys. If nothing else in the program is referencing a key, the garbage collector is free to discard it — and the entry vanishes from the collection. That sounds spooky, but it is exactly what you want when you're attaching metadata to objects you don't own.
Why weak references matter
Imagine you want to remember which DOM nodes you've already initialised. With a normal Set, adding a node would keep it alive forever, even after it has been removed from the page. That's a memory leak.
The leak — Set keeps everything alive
const initialised = new Set();
function init(node) {
if (initialised.has(node)) return;
initialised.add(node); // node now reachable from the Set
attachStuff(node);
}
// remove the node from the DOM, drop your reference...
// the Set still holds it. Memory grows over time.The fix — WeakSet lets the node be collected
const initialised = new WeakSet();
function init(node) {
if (initialised.has(node)) return;
initialised.add(node); // weak reference — does not prevent GC
attachStuff(node);
}
// once the node is no longer reachable elsewhere,
// it's automatically removed from the WeakSet.That is the whole pitch: WeakSet/WeakMap let you tag or annotate objects without extending their lifetime.
WeakSet
A WeakSet is a set of objects. It supports just three methods:
ws.add(obj)— add. Returns the WeakSet.ws.has(obj)— membership check.ws.delete(obj)— remove.
const visited = new WeakSet();
function walk(node) {
if (visited.has(node)) return;
visited.add(node);
for (const child of node.children ?? []) walk(child);
}Use cases: marking objects as "seen" during traversal, tracking which instances have been initialised, opting objects in or out of behaviour without mutating them.
WeakMap
A WeakMap is a map whose keys are objects (the values can be anything). You get the same four core methods as Map:
wm.set(keyObj, value)wm.get(keyObj)wm.has(keyObj)wm.delete(keyObj)
Per-instance private data
const privateData = new WeakMap();
class User {
constructor(name, secret) {
this.name = name;
privateData.set(this, { secret });
}
getSecret() {
return privateData.get(this).secret;
}
}
const u = new User("Ada", "42");
console.log(u.getSecret()); // 42
// u.secret is undefined — nothing leaks onto the instance.When u is no longer reachable, the entry in privateData is collected too. No explicit cleanup, no leak.
Key restrictions
Both collections enforce strict rules on what may be a key:
Keys must be objects (or, since ES2023, non-registered symbols). Primitives like strings and numbers are rejected.
Keys are compared by reference, like in
Set/Map.You cannot list the entries, get the size, or iterate. There is no
size, nokeys(), noforEach, nofor...of.
const wm = new WeakMap();
wm.set({}, 1); // ok — object literal
wm.set(Symbol("name"), 1); // ok — non-registered symbol
// wm.set("a", 1); // TypeError: Invalid value used as weak map key
// wm.set(42, 1); // TypeErrorReal-world use cases
Caching computed results keyed by an object — the cache entry disappears when the object does.
Per-instance private state before private class fields existed (and still useful for keeping data fully external to instances).
Marking DOM nodes with metadata without polluting their property bag or risking leaks.
Visited sets in graph or DOM traversals when the graph might be large or long-lived.
Listeners and subscriptions keyed by the subject object, so cleanup happens for free.
Memoise expensive work per object
const layouts = new WeakMap();
function getLayout(node) {
if (layouts.has(node)) return layouts.get(node);
const layout = computeLayout(node); // expensive
layouts.set(node, layout);
return layout;
}When NOT to use them
Weak collections look powerful but they are narrow tools. Pick a regular Set or Map when:
You need to iterate the collection or know its size.
Your keys are strings, numbers, or other primitives.
You want the entries to stay alive — e.g. a long-term cache the rest of the program looks things up in.
You need to serialise the collection.
FinalizationRegistry — a related cousin
If you need a callback when an object is garbage-collected, there's FinalizationRegistry. It's even more advanced and easy to misuse — most apps will never need it. It exists for libraries that wrap external resources (file handles, native bindings) and need a last-chance cleanup hook.