WeakMap, WeakSet, WeakRef, and FinalizationRegistry
The Problem: References That Keep Things Alive Too Long
You've learned that any reachable reference prevents garbage collection. But here's a scenario that comes up constantly: you want to associate data with an object — metadata, cached computations, DOM element state — without preventing the object from being collected when it's no longer needed elsewhere.
// PROBLEM: This map keeps every user object alive forever
const userMetadata = new Map();
function trackUser(user) {
userMetadata.set(user, {
firstSeen: Date.now(),
interactions: 0
});
}
// Even if the original 'user' object is no longer used anywhere,
// the Map key keeps it reachable: global → userMetadata → key (user) → user object
// The metadata entry AND the user both leak
You need a way to say: "store this association, but if the key object is garbage collected by everyone else, let the entry disappear too." Sounds reasonable, right?
That's exactly what weak references provide.
WeakMap: Metadata Without Ownership
Think of a WeakMap as sticky notes attached to objects. The sticky note (value) is associated with the object (key), but the note doesn't prevent the object from being thrown away. When the object goes to the trash, the sticky note disappears with it. You can't list all the sticky notes — you can only check a specific object for its note.
A WeakMap is like a Map, but with critical differences:
| Property | Map | WeakMap |
|---|---|---|
| Key types | Any value | Objects and Symbols only |
| Prevents GC of keys | Yes | No |
| Iterable | Yes (.keys(), .values(), .entries(), for...of) | No |
.size property | Yes | No |
| Use case | General key-value storage | Associating metadata with objects you don't own |
const metadata = new WeakMap();
function processElement(element) {
// Associate data with the element without preventing its collection
if (!metadata.has(element)) {
metadata.set(element, {
processedAt: Date.now(),
computedLayout: computeExpensiveLayout(element)
});
}
return metadata.get(element);
}
// When 'element' is removed from the DOM and all other references are gone,
// the WeakMap entry is automatically removed — no manual cleanup needed
Why WeakMap Can't Be Iterated
This trips people up. "Why can't I loop over a WeakMap?" It's a deliberate design choice, not a limitation. If you could iterate a WeakMap's keys, you'd hold references to all of them during iteration — which would prevent garbage collection, defeating the entire purpose. The GC must be free to collect any key at any time, so the WeakMap cannot expose its key set.
Real-World WeakMap Patterns
Private data for class instances:
const _private = new WeakMap();
class User {
constructor(name, email) {
_private.set(this, { email, passwordHash: null });
this.name = name;
}
getEmail() {
return _private.get(this).email;
}
}
const user = new User('Alice', 'alice@example.com');
user.name; // 'Alice' — public
user.email; // undefined — not on the instance
user.getEmail(); // 'alice@example.com' — accessible through WeakMap
// When 'user' is GC'd, the private data is automatically cleaned up
Caching computed results per object:
const computeCache = new WeakMap();
function expensiveComputation(obj) {
if (computeCache.has(obj)) return computeCache.get(obj);
const result = /* ... heavy computation ... */ obj.data.reduce((a, b) => a + b, 0);
computeCache.set(obj, result);
return result;
}
// Cache entries disappear when the input object is GC'd
// No manual cache invalidation needed
WeakSet: Membership Without Ownership
A WeakSet tracks whether objects belong to a group — without preventing their collection.
const visited = new WeakSet();
function processNode(node) {
if (visited.has(node)) return; // already processed
visited.add(node);
// Process the node...
node.children.forEach(processNode);
}
// When a node is GC'd, it's automatically removed from the WeakSet
// No risk of the Set growing unboundedly
Branding / type-checking:
const validatedInputs = new WeakSet();
function validate(input) {
// ... validation logic ...
if (isValid) {
validatedInputs.add(input);
}
return isValid;
}
function submitForm(formData) {
if (!validatedInputs.has(formData)) {
throw new Error('Form data must be validated first');
}
// Safe to submit
}
WeakRef: Soft Caches and Nullable References
WeakMap and WeakSet are great when your keys are objects. But what if you want a reference to an object that doesn't prevent garbage collection, period? WeakRef gives you exactly that. You can check if the object is still alive by calling .deref():
let target = { data: 'important', payload: new ArrayBuffer(10_000_000) };
const weakRef = new WeakRef(target);
// Later...
const obj = weakRef.deref();
if (obj) {
console.log(obj.data); // object is still alive
} else {
console.log('Object was garbage collected');
}
Building a WeakRef-Based Cache
class WeakCache {
#cache = new Map();
get(key) {
const ref = this.#cache.get(key);
if (!ref) return undefined;
const value = ref.deref();
if (!value) {
// Object was collected — clean up the stale WeakRef
this.#cache.delete(key);
return undefined;
}
return value;
}
set(key, value) {
this.#cache.set(key, new WeakRef(value));
}
}
const imageCache = new WeakCache();
function loadImage(url) {
const cached = imageCache.get(url);
if (cached) return cached;
const img = createLargeImageObject(url);
imageCache.set(url, img);
return img;
}
// If memory pressure causes the GC to collect an image,
// the cache gracefully returns undefined and re-fetches
WeakRef.deref() may return the object even after it becomes unreachable — the GC hasn't run yet. And after a GC cycle, it may return undefined even though the object "should" still be alive (V8 decides when to collect). Never rely on precise timing of WeakRef invalidation. Treat it as: "the object might be alive." Design code that handles both cases.
The TC39 proposal explicitly warns: "Correct use of WeakRef takes careful thought, and they are best avoided if possible." WeakRefs make program behavior depend on GC timing, which is non-deterministic. Use them for caches where a miss is acceptable, not for correctness-critical logic.
FinalizationRegistry: Cleanup Callbacks
Okay, this is the most exotic tool in the box. FinalizationRegistry lets you register a callback that fires when a registered object is garbage collected. It's the closest JavaScript gets to a destructor — and it comes with major caveats.
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Object associated with "${heldValue}" was collected`);
// Perform cleanup: close connections, release external resources, etc.
});
function createResource(name) {
const resource = { name, handle: openExternalHandle() };
// When 'resource' is GC'd, the callback fires with 'name' as heldValue
registry.register(resource, name);
return resource;
}
let r = createResource('database-connection');
r = null; // resource becomes eligible for GC
// Eventually: "Object associated with 'database-connection' was collected"
Practical FinalizationRegistry: External Resource Cleanup
const fileHandleRegistry = new FinalizationRegistry((handle) => {
// Close the file handle when the wrapper object is GC'd
handle.close();
console.log(`Auto-closed file handle ${handle.id}`);
});
class ManagedFile {
#handle;
constructor(path) {
this.#handle = openFile(path);
// Register this instance — when GC'd, close the handle
fileHandleRegistry.register(this, this.#handle);
}
read() {
return this.#handle.readAll();
}
// Explicit close is still preferred — FinalizationRegistry is a safety net
close() {
this.#handle.close();
fileHandleRegistry.unregister(this); // no need for callback anymore
}
}
FinalizationRegistry timing guarantees (or lack thereof)
The spec makes almost no timing guarantees:
- The callback may fire at any point after the object becomes unreachable — could be milliseconds, seconds, or never (if the page closes first).
- The callback runs as a microtask, so it won't interrupt synchronous code, but it may run between tasks.
- V8 typically fires finalization callbacks during GC, but this is an implementation detail.
- Multiple collected objects' callbacks may be batched.
This means FinalizationRegistry is a best-effort safety net, not a guaranteed cleanup mechanism. Always provide an explicit close() or dispose() method. Use FinalizationRegistry as a fallback for when users forget to call it.
WeakMap vs WeakRef: When to Use Which
With all these tools available, here's how to pick the right one.
| Scenario | Use |
|---|---|
| Associate metadata with objects you don't own | WeakMap |
Private fields (pre-#private syntax) | WeakMap |
| Track whether objects belong to a set | WeakSet |
| Cache computed results keyed by object identity | WeakMap |
| Cache with string keys where values may be GC'd | WeakRef (values are weak) |
| Build a soft-reference cache (evict under memory pressure) | WeakRef + FinalizationRegistry |
| Clean up external resources when wrapper objects die | FinalizationRegistry |
| What developers do | What they should do |
|---|---|
| Using Map when keys are objects that should be GC-eligible Map keys are strong references that prevent GC, causing memory leaks for object-keyed caches | Use WeakMap — entries are automatically removed when keys are collected |
| Trying to iterate a WeakMap or check its size Iteration would create strong references to all keys, defeating the weak reference purpose | WeakMap is intentionally non-iterable. If you need iteration, use Map with manual cleanup. |
| Relying on WeakRef.deref() timing for correctness GC timing is non-deterministic. The object may survive longer than expected or be collected sooner. | Treat WeakRef.deref() as 'might be alive' — always handle the undefined case |
| Using FinalizationRegistry as the primary cleanup mechanism Finalization callbacks have no timing guarantees — they may fire late or not at all before page unload | Provide explicit close/dispose methods. Use FinalizationRegistry as a safety net for forgotten cleanup. |
- 1WeakMap keys are weak references — entries are auto-removed when the key object is GC'd. Use for metadata, caches, and private data keyed by object identity.
- 2WeakSet tracks membership without preventing GC. Use for visited/processed/validated tracking.
- 3WeakRef gives a non-preventing reference to any object. deref() returns the object or undefined. Use for soft caches where misses are acceptable.
- 4FinalizationRegistry fires a callback when a registered object is collected. No timing guarantees — use as a safety net, not primary cleanup.
- 5Always provide explicit cleanup methods (close, dispose, destroy). Weak references and finalization are fallbacks, not replacements.
- 6WeakMap and WeakSet cannot be iterated, sized, or cleared — this is by design, not a limitation.