A coworker once asked me, "When have you actually used WeakMap?" The question was honest; the language adds features faster than most engineers find a reason to reach for them, and WeakMap and WeakSet are easy to file under "obscure". I had to think for a moment before answering, and the honest answer was: about three times in five years, but in each of those cases, no other tool would have done the job correctly. That ratio is what I want to defend in this piece. WeakMap and WeakSet are not features you reach for daily; they are the right answer to a small set of structural problems where the wrong answer is a memory leak.
The opinion: weak collections solve "associate metadata with an object I do not own and cannot modify" without leaking memory. Use them when you cannot put a property on the target object directly, when the target object's lifetime is shorter than your cache's, and when GC is allowed to remove entries without your code noticing.
What "weak" actually means
A Map keeps a strong reference to every key. As long as the map is reachable, every key it holds is reachable, which means the GC will never collect those keys. This is what you want when the map is the source of truth for the object's existence (a cache that owns its values, a registry that defines who is registered).
A WeakMap holds keys weakly. The map's reference to the key does not prevent the key from being collected. If the key becomes unreachable from anywhere else in the program, the GC reclaims it, and the map's entry quietly disappears. The map cannot be enumerated; you cannot ask "give me all the keys" because the engine reserves the right to remove keys at any time without telling you.
That single property (the map does not keep the key alive) is what makes WeakMap appropriate for "metadata associated with an external object's lifetime". The metadata lives as long as the object lives, and then both go away together. No manual cleanup, no listener-based timing, no leak.
Constraint: keys must be objects
WeakMap keys must be objects (or, since recent specs, non-registered Symbols). You cannot use a string or number as a key. The reason is structural: GC tracks reachability of objects, not of primitive values. A primitive cannot be "collected"; it does not exist as a heap allocation. Without a heap object to track, the engine has no signal to fire the entry-removal logic.
This constraint is the entire reason WeakMap is not a drop-in Map replacement. The keyset is restricted to objects, and the API is restricted to set, get, has, delete. No iteration, no size, no clear. If you need any of those, you need a regular Map and a manual cleanup strategy.
Where I have used WeakMap in production
Three real cases, in order of how often they show up.
Per-instance metadata that the user cannot store on the instance. I have written libraries that consume third-party objects (DOM nodes, framework instances, options objects passed in) and need to attach extra state to each one. Putting the state as a property on the object would pollute the public surface and could collide with the user's own keys. The right solution is a module-private WeakMap keyed by the instance.
The DOM node owns its lifetime. When it is removed from the document and dropped by every reference, the WeakMap entry vanishes with it. No detach API, no cleanup step, no listener leaks.
Memoization keyed by an object argument. If the function takes an object, holding the cache as a Map would keep every input object alive forever. A WeakMap lets the cache size scale with reachable inputs only.
This is a niche but exact pattern: the input object is alive somewhere in the program, the cached result lives alongside it, and when the input goes away the cached result goes too. If the cache key were a primitive, this would not work; the standard memoization pattern with object identity by reference is exactly what WeakMap enables.
Private state for class instances, before private fields existed. This was the workhorse pattern for "real" private state in pre-ES2022 codebases: a module-level WeakMap keyed by this in each method.
Modern code uses class private fields (#count) for this; the engine implements similar isolation natively. But the WeakMap pattern still appears in libraries that target older runtimes, and reading it should not be unfamiliar.
Where WeakSet earns its keep
WeakSet is the same idea with Map replaced by Set. It holds a weak reference to each member. You cannot iterate; you cannot get a size; you cannot enumerate. The supported operations are add, has, delete.
The case I have actually shipped: tracking "have I seen this object before" without rooting it. A cycle-detector for serialisation, for example, needs to know whether the current object has been visited during the current traversal. A regular Set would keep every visited object alive for the duration of the program. A WeakSet does not, and once the traversal finishes and you drop the set, every visited object is reclaimable as if the traversal had never happened.
The seen set is local to the call; once the function returns, the WeakSet itself is garbage. For a one-shot cycle detector this is overkill (a Set would also be collected at function return). The case where it matters is when the cycle detector lives across calls, like a long-running serialiser that processes many independent values and wants to remember which deeper objects it has already seen, without holding them all alive permanently.
What WeakMap is not for
The most common misuse I see is treating WeakMap like a "private property bag" and assuming the GC will reclaim entries deterministically. The spec says nothing about when entries are removed. An engine is free to keep entries around until the next major collection, which on a long-lived process can be minutes. If you write code that depends on "the entry will be gone shortly after the key drops out of scope", you have written a race condition.
The other misuse is thinking weak references will fix a memory leak in code that holds strong references elsewhere. If the same object is being kept alive by an event listener, a closure, a global registry, or a child component that grandparented up to a singleton, the WeakMap is irrelevant; the GC will not collect the object, and the WeakMap entry will stay forever. WeakMap shifts the leak from "the map itself" to "wherever the strong reference still lives". It does not delete leaks; it relocates the responsibility for finding them.
The third misuse is reaching for WeakMap when a regular Map plus an explicit invalidation step would be clearer. If the lifecycle is "create, use, dispose", a regular Map with a dispose method that calls map.delete(key) is more obvious than a WeakMap that relies on GC. Future readers will appreciate the explicit cleanup over the magical disappearance.
When I reach for them, condensed
Three triggers that flip me from "regular Map/Set" to "weak":
| Trigger | Reach for |
|---|---|
| Need to associate state with an object I do not own | WeakMap |
| Memoize a function that takes an object argument | WeakMap |
| Track "seen this object" across calls without rooting it | WeakSet |
| Need iteration, size, or enumeration | NOT WeakMap/WeakSet (use Map/Set) |
| Need deterministic cleanup timing | NOT WeakMap/WeakSet (use explicit disposal) |
The triggers are narrow and the API surface is small. That is the entire feature. Used correctly, weak collections solve a class of memory-management problems that no other built-in solves. Used incorrectly, they paper over leaks instead of preventing them. The discipline is recognising which case you are in.
The correct WeakMap is invisible
The win from a well-placed WeakMap is that you stop thinking about the cleanup step entirely. The state lives as long as the host object lives; both go away together; the code reads as if the state were a private field on the host, even when no such field exists. The wrong WeakMap, by contrast, becomes a debugging tax: you add it expecting a leak fix, then spend the next sprint hunting why the heap is still growing.
Reach for WeakMap when the problem is "metadata associated with an external object's natural lifetime", not "I want a Map that goes away eventually". Those two problems look similar from a distance; only the first one is what weak collections solve.
