JavaScriptWeakSet & WeakMap

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

JS
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

JS
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.

JS
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

JS
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, no keys(), no forEach, no for...of.

JS
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);               // TypeError
No iteration — by design
Iteration would let you observe garbage collection timing, which would be a disaster for both performance and language stability. The lack of `size` and `for...of` isn't an oversight — it's load-bearing.
Real-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

JS
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.

GC timing is invisible
You cannot tell when a weak entry will be collected — only that it eventually can be. Don't write code that depends on a key disappearing at a particular moment. Treat WeakSet/WeakMap as "this entry exists *at most* as long as its key does".
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.

One sentence to remember
Use `WeakMap`/`WeakSet` to attach metadata to objects **you don't want to keep alive** — they're the right answer whenever a normal `Map`/`Set` would leak.