Skip to content
C Codeloom
JavaScript

JavaScript WeakMap and WeakSet Explained

What WeakMap and WeakSet are, how their garbage collection semantics differ from Map and Set, and where they actually pay off in real code.

·5 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • How WeakMap and WeakSet differ from Map and Set
  • What "weak reference" actually means here
  • Use cases like private data and metadata
  • Memory leak patterns they prevent
  • Limitations and how to work around them

Prerequisites

  • Comfortable with JavaScript objects

Map and Set keep their keys alive forever, which is fine until you start using them for associating extra data with objects whose lifetime you do not control. WeakMap and WeakSet solve that specific problem: they hold references that the garbage collector is allowed to ignore. Used in the right places, they prevent leaks without requiring manual cleanup.

What and why

A WeakMap is a key-value collection where the keys must be objects (or symbols, in modern engines) and the references to those keys are “weak.” If no other code holds a reference to a key, the garbage collector is free to remove that key (and its associated value) from the map. You never see it disappear; it just stops being there.

A WeakSet is the analogous structure for unique objects without values.

The reason this matters: if you use a regular Map keyed by DOM elements to store per-element data, those elements can never be garbage collected, because the map holds a strong reference. Replace it with a WeakMap and the cleanup happens automatically when the element is removed from the DOM.

Mental model

A Map is like a parking lot where every parked car is held forever. A WeakMap is more like a “may park here” sign: if the car drives away (no other code references it), its spot is reclaimed.

Map:
map -----strong-----> key (kept alive)
                      |
                      +--> value

WeakMap:
weakmap --weak--> key (collectible)
                  |
                  +--> value (gone when key is collected)

if (no other strong ref to key) -> GC removes both
Reference strength

Hands-on example

Associating private data with an object without polluting it:

const _internal = new WeakMap();

class Counter {
  constructor() {
    _internal.set(this, { count: 0 });
  }
  inc() {
    const state = _internal.get(this);
    state.count++;
    return state.count;
  }
}

const c = new Counter();
c.inc(); c.inc(); // 1, 2

When the Counter instance is no longer referenced, both the instance and its entry in _internal are eligible for collection. No leak.

Caching expensive computations per object:

const cache = new WeakMap();

function expensive(node) {
  if (cache.has(node)) return cache.get(node);
  const result = computeFrom(node);
  cache.set(node, result);
  return result;
}

If node is a DOM element that gets detached, the cache entry vanishes too.

Tracking visited objects in graph traversal:

function deepClone(obj, seen = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (seen.has(obj)) return seen.get(obj); // handle cycles
  const copy = Array.isArray(obj) ? [] : {};
  seen.set(obj, copy);
  for (const key of Object.keys(obj)) copy[key] = deepClone(obj[key], seen);
  return copy;
}

The seen WeakMap lets the function detect cycles without keeping every visited node alive after the call returns.

Limitations

WeakMap has no size, no iteration, no forEach, no keys(). The reason: those operations would require deterministic visibility into what is currently alive, which contradicts the whole point of being weak. You cannot ask “how many entries are there” because the answer would change every time the garbage collector ran.

If you need iteration, you need a regular Map, possibly with manual cleanup hooks. There is a FinalizationRegistry for advanced cases, but it has its own warnings — finalizers run on a “best effort” basis and should not hold critical logic.

Common pitfalls

  • Trying to use primitives as keys. Strings, numbers, and booleans throw a TypeError. Only objects (and modern symbols) are allowed.
  • Holding the key strongly elsewhere. If you store the same object in a regular array or Map, the weak reference does not help — the strong reference dominates.
  • Relying on garbage collection timing. You cannot test that a WeakMap entry has been collected; the GC has no public hook for it.
  • Using WeakMap to store global state. It only makes sense when the keys have a meaningful lifetime; if the key is a singleton, a regular property is simpler.

Best practices

  • Use WeakMap for associating data with objects you do not own (DOM nodes, third-party objects, library inputs).
  • Use WeakSet for “have I seen this object?” checks where you want the set to drop entries automatically.
  • Document the weakness. Future readers may not realize an absent entry just means the key was collected.
  • For cycle-detection inside short-lived recursive functions, a plain Set is fine. WeakSet becomes useful when the recursion outlives the immediate call.

FAQ

Why can WeakMap not be iterated? Because iteration order would expose garbage collection timing, which is intentionally non-deterministic.

Are WeakMap operations slower than Map? Roughly similar in practice. The difference is the GC interaction, not per-operation speed.

Can a value in a WeakMap hold a strong reference to its key? No. The runtime ensures the key-to-value relationship does not create a cycle that keeps the key alive.

When should I use FinalizationRegistry instead? When you need to perform cleanup (closing a file handle, removing a listener) after an object is collected. Use it sparingly; finalizers are not guaranteed to run.