WeakRef, WeakMap, and WeakSet
References That Don't Prevent Garbage Collection
Here's a scenario you've probably dealt with: you want to attach some metadata to a DOM element — maybe a click count, or a timestamp. You could use a Map, but what happens when that element gets removed from the page? If your Map still holds a reference to it, the element (and your metadata) stay in memory forever. That's a memory leak.
WeakMap, WeakSet, and WeakRef solve this problem. They hold references to objects that don't count for garbage collection. If the object is no longer reachable through any other path, the garbage collector is free to collect it — even though a WeakMap/WeakSet/WeakRef still points to it.
Normal references are like handcuffs — as long as you're holding on, the object can't leave. Weak references are like sticky notes — you've written a reference to the object, but if the object leaves (gets garbage collected), the sticky note just goes blank. You held onto the note, but not the object.
WeakMap — The Right Way to Attach Metadata
Let's start with the one you'll use most often. A WeakMap is a key-value store where keys must be objects (or non-registered symbols), and keys are held weakly:
const metadata = new WeakMap();
function processElement(element) {
// Attach metadata to a DOM element without modifying it
metadata.set(element, {
processedAt: Date.now(),
clickCount: 0
});
}
function handleClick(element) {
const data = metadata.get(element);
if (data) data.clickCount++;
}
// When the DOM element is removed and no other references exist,
// the WeakMap entry is automatically garbage collected.
// No manual cleanup needed. No memory leak.
WeakMap vs Map — The Critical Difference
// Map holds STRONG references to keys
const cache = new Map();
function processWithMap(obj) {
cache.set(obj, expensiveComputation(obj));
}
// Even after all other references to obj are gone,
// the Map keeps it alive → MEMORY LEAK
// WeakMap holds WEAK references to keys
const cache = new WeakMap();
function processWithWeakMap(obj) {
cache.set(obj, expensiveComputation(obj));
}
// When obj is no longer referenced elsewhere,
// both the key and value are garbage collected
Why WeakMap Keys Must Be Objects
This restriction surprises people, but it makes perfect sense once you think about it. Primitives (numbers, strings, booleans) are not garbage collected the way objects are — they're managed by value, not by reference. There's no way to "weakly reference" the number 42 because 42 doesn't have a unique identity in memory.
const wm = new WeakMap();
wm.set(42, "data"); // TypeError: Invalid value used as weak map key
wm.set("hello", "data"); // TypeError
wm.set({}, "data"); // OK — objects have unique identity
wm.set(Symbol("x"), "data"); // OK in modern engines (non-registered symbols)
WeakMap Is Not Enumerable
You cannot iterate over a WeakMap. No .keys(), .values(), .entries(), .forEach(), no .size:
const wm = new WeakMap();
wm.set({}, "a");
wm.size; // undefined
[...wm]; // TypeError: wm is not iterable
wm.keys(); // TypeError: wm.keys is not a function
This isn't a limitation — it's a design requirement. If WeakMaps were enumerable, you could observe when entries disappear due to garbage collection. But garbage collection is non-deterministic — it can happen at any time. Making it observable would create race conditions and make program behavior depend on GC timing, which would be a nightmare. The non-enumerability guarantees that GC timing never affects program logic.
WeakMap Use Cases
Private Data (Pre-#private fields)
const _private = new WeakMap();
class Person {
constructor(name, ssn) {
this.name = name;
_private.set(this, { ssn }); // Private data attached externally
}
getSSN(authorized) {
if (!authorized) throw new Error("Unauthorized");
return _private.get(this).ssn;
}
}
const p = new Person("Alice", "123-45-6789");
p.name; // "Alice" — public
p.ssn; // undefined — not on the object
_private.get(p); // { ssn: "123-45-6789" } — only accessible through the WeakMap
// When p is garbage collected, the private data is too
DOM Node Metadata
const nodeData = new WeakMap();
function trackElement(element) {
nodeData.set(element, {
impressionTime: Date.now(),
viewed: false,
interactions: 0
});
}
// When the element is removed from the DOM and dereferenced,
// the metadata is automatically cleaned up. Zero manual bookkeeping.
Memoization Without Memory Leaks
const cache = new WeakMap();
function expensiveOperation(obj) {
if (cache.has(obj)) return cache.get(obj);
const result = /* heavy computation based on obj */;
cache.set(obj, result);
return result;
}
// Cache entries for unreachable objects are automatically cleaned up
WeakSet — Object Tagging
WeakSet is like Set but with weak references and no enumeration. It's used for marking or tagging objects:
const visited = new WeakSet();
function processOnce(obj) {
if (visited.has(obj)) return; // Already processed
visited.add(obj);
// Do expensive processing...
}
// No need to clean up visited — entries for unreachable objects
// are automatically garbage collected
Circular Reference Detection
function deepClone(obj, seen = new WeakSet()) {
if (obj === null || typeof obj !== "object") return obj;
if (seen.has(obj)) throw new Error("Circular reference detected");
seen.add(obj);
const clone = Array.isArray(obj) ? [] : {};
for (const key of Reflect.ownKeys(obj)) {
clone[key] = deepClone(obj[key], seen);
}
return clone;
}
WeakRef — Soft References
Now we're getting into the advanced territory. WeakRef provides a weak reference to a single object, with the ability to check if it's still alive:
let target = { data: "important" };
const ref = new WeakRef(target);
ref.deref(); // { data: "important" } — still alive
target = null; // Remove the strong reference
// ... at some point after GC runs ...
ref.deref(); // undefined — the object was collected
The spec explicitly warns: "Correct use of WeakRef requires careful thought, and they are best avoided if possible." The behavior depends on garbage collection timing, which is non-deterministic and varies between engines, versions, and heap pressure. Never use WeakRef in logic that requires the object to be alive or dead at a specific time.
WeakRef Cache Pattern
class Cache {
#map = new Map();
set(key, value) {
this.#map.set(key, new WeakRef(value));
}
get(key) {
const ref = this.#map.get(key);
if (!ref) return undefined;
const value = ref.deref();
if (value === undefined) {
// Object was garbage collected — clean up the dead entry
this.#map.delete(key);
return undefined;
}
return value;
}
}
FinalizationRegistry — Cleanup Callbacks
This one's niche, but when you need it, nothing else will do. FinalizationRegistry lets you register a callback that runs when an object is garbage collected:
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Object associated with "${heldValue}" was collected`);
// Clean up external resources: close file handles, deregister from server, etc.
});
let obj = { name: "Alice" };
registry.register(obj, "alice-data"); // "alice-data" is the held value
obj = null;
// Eventually, after GC: logs "Object associated with 'alice-data' was collected"
FinalizationRegistry for resource cleanup
The primary use case is cleaning up external resources that JavaScript's GC doesn't know about:
const fileRegistry = new FinalizationRegistry((fd) => {
// Close the file descriptor when the JS wrapper is collected
closeFileDescriptor(fd);
});
class FileHandle {
#fd;
constructor(path) {
this.#fd = openFile(path);
fileRegistry.register(this, this.#fd);
}
read() { return readFile(this.#fd); }
close() {
closeFileDescriptor(this.#fd);
fileRegistry.unregister(this); // Prevent double-close
}
}But don't rely on this for critical cleanup — the spec doesn't guarantee the callback will ever run (the program might exit first). Always provide an explicit close() method too.
Production Scenario: Preventing Event Listener Leaks
const listenerRegistry = new WeakMap();
function safeAddEventListener(element, event, handler) {
// Track listeners per element
if (!listenerRegistry.has(element)) {
listenerRegistry.set(element, []);
}
listenerRegistry.get(element).push({ event, handler });
element.addEventListener(event, handler);
}
function removeAllListeners(element) {
const listeners = listenerRegistry.get(element) || [];
for (const { event, handler } of listeners) {
element.removeEventListener(event, handler);
}
listenerRegistry.delete(element);
}
// When element is removed from the DOM and dereferenced,
// the WeakMap entry (including the listener list) is cleaned up.
// No manual cleanup needed for the WeakMap itself.
| What developers do | What they should do |
|---|---|
| Using Map when keys are objects that should be garbage collected Map holds strong references to keys, preventing garbage collection and causing memory leaks | Use WeakMap — entries are automatically cleaned when keys are unreachable |
| Trying to iterate over WeakMap/WeakSet Enumerability would expose GC timing, creating non-deterministic program behavior | WeakMap/WeakSet are not enumerable by design — use Map/Set if you need iteration |
| Using WeakRef in logic that depends on the object being alive GC timing is non-deterministic. WeakRef objects can be collected at any time. | Always handle the case where deref() returns undefined. Use strong references when the object must be alive. |
| Using primitive keys with WeakMap Primitives don't have unique identity — they're compared by value, not reference | WeakMap keys must be objects (or non-registered symbols). Use Map for primitive keys. |
- 1WeakMap/WeakSet hold weak references — entries are automatically cleaned when keys become unreachable
- 2WeakMap keys must be objects or non-registered symbols. Values are held strongly.
- 3WeakMap/WeakSet are not enumerable — this is by design to keep GC timing unobservable
- 4WeakRef.deref() can return undefined at any time — always handle both cases
- 5FinalizationRegistry callbacks are not guaranteed to run — always provide explicit cleanup methods too