FinalizationRegistry and WeakRef in JavaScript
Learn how to use WeakRef and FinalizationRegistry for custom cleanup callbacks, weak caching, and observing garbage collection in JavaScript.
What you'll learn
- ✓How WeakRef creates references that do not prevent garbage collection
- ✓Using FinalizationRegistry to run cleanup when objects are collected
- ✓Building weak caches and resource managers with these APIs
Prerequisites
- •JavaScript objects and memory management basics
- •Understanding of garbage collection concepts
- •Familiarity with WeakMap and WeakSet
JavaScript is a garbage-collected language — objects are automatically freed when no strong references point to them. WeakRef and FinalizationRegistry, introduced in ES2021, give you controlled interaction with the garbage collector: weak references that do not prevent collection, and callbacks that fire when objects are collected.
WeakRef basics
A WeakRef wraps an object without preventing it from being garbage collected. You access the underlying object via .deref(), which returns undefined if the object has been collected.
let largeData = { values: new Array(1_000_000).fill(0) };
const weakRef = new WeakRef(largeData);
console.log(weakRef.deref()); // { values: [...] } -- still alive
// Remove the strong reference
largeData = null;
// At some point after GC runs:
// weakRef.deref() will return undefined
Important: garbage collection timing is non-deterministic. You cannot predict exactly when deref() will start returning undefined. The spec guarantees that within a single synchronous execution (a single “turn” of the event loop), deref() will return a consistent result.
Building a weak cache
The most practical use of WeakRef is building a cache that does not prevent its entries from being garbage collected when memory pressure is high.
class WeakCache {
#cache = new Map();
set(key, value) {
this.#cache.set(key, new WeakRef(value));
}
get(key) {
const ref = this.#cache.get(key);
if (!ref) return undefined;
const value = ref.deref();
if (value === undefined) {
// Object was garbage collected, clean up the entry
this.#cache.delete(key);
return undefined;
}
return value;
}
has(key) {
return this.get(key) !== undefined;
}
}
const cache = new WeakCache();
function fetchAndCache(url) {
const cached = cache.get(url);
if (cached) {
console.log("Cache hit");
return cached;
}
console.log("Cache miss -- fetching");
const data = { url, timestamp: Date.now() }; // Simulated fetch result
cache.set(url, data);
return data;
}
let result = fetchAndCache("/api/users"); // Cache miss
result = fetchAndCache("/api/users"); // Cache hit
The weak cache allows the garbage collector to reclaim cached objects under memory pressure, preventing the cache from growing unbounded.
FinalizationRegistry
FinalizationRegistry lets you register a callback that fires when a registered object is garbage collected. This is useful for cleaning up external resources associated with an object.
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Object associated with "${heldValue}" was collected`);
});
let obj = { name: "temporary" };
registry.register(obj, "my-object");
// Remove the strong reference
obj = null;
// Eventually, after GC: "Object associated with 'my-object' was collected"
The second argument to register() is the “held value” — data passed to the callback. It must not be the registered object itself (that would create a strong reference and prevent collection).
Unregistering
You can pass an unregister token to register() and later call unregister() to cancel the callback.
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Cleaning up: ${heldValue}`);
});
let obj = { data: "important" };
const token = {}; // Any object can serve as the token
registry.register(obj, "resource-123", token);
// If we clean up manually, unregister to prevent the callback
registry.unregister(token);
Practical example: file handle manager
Suppose you have objects that hold external resources (like file handles or database connections). FinalizationRegistry ensures cleanup even if the developer forgets to call a close() method.
class FileHandleManager {
#registry;
#openHandles = new Map();
constructor() {
this.#registry = new FinalizationRegistry((handleId) => {
console.warn(`File handle ${handleId} was not closed -- cleaning up`);
this.#closeHandle(handleId);
});
}
open(filePath) {
const handleId = crypto.randomUUID();
const handle = { id: handleId, path: filePath, fd: this.#openFile(filePath) };
const token = { handleId };
this.#openHandles.set(handleId, { fd: handle.fd, token });
this.#registry.register(handle, handleId, token);
return handle;
}
close(handle) {
const entry = this.#openHandles.get(handle.id);
if (entry) {
this.#closeHandle(handle.id);
this.#registry.unregister(entry.token);
}
}
#openFile(path) {
// Simulated file open
return Math.floor(Math.random() * 1000);
}
#closeHandle(handleId) {
const entry = this.#openHandles.get(handleId);
if (entry) {
console.log(`Closing file descriptor ${entry.fd}`);
this.#openHandles.delete(handleId);
}
}
}
const manager = new FileHandleManager();
// Good: explicit cleanup
const handle1 = manager.open("/tmp/data.txt");
manager.close(handle1);
// Bad but safe: forgot to close -- FinalizationRegistry cleans up
let handle2 = manager.open("/tmp/leak.txt");
handle2 = null; // Eventually triggers cleanup callback
Combining WeakRef and FinalizationRegistry
A robust weak cache uses both: WeakRef for the cache entries and FinalizationRegistry to automatically remove stale map keys.
class AutoCleaningCache {
#cache = new Map();
#registry;
constructor() {
this.#registry = new FinalizationRegistry((key) => {
// Only delete if the current entry is still the collected one
const ref = this.#cache.get(key);
if (ref && ref.deref() === undefined) {
this.#cache.delete(key);
}
});
}
set(key, value) {
const existingRef = this.#cache.get(key);
if (existingRef) {
const existing = existingRef.deref();
if (existing) {
this.#registry.unregister(existing);
}
}
this.#cache.set(key, new WeakRef(value));
this.#registry.register(value, key, value);
}
get(key) {
const ref = this.#cache.get(key);
return ref?.deref();
}
get size() {
return this.#cache.size;
}
}
This cache automatically shrinks as objects are garbage collected, without requiring manual cleanup or periodic sweeps.
Important caveats
These APIs come with significant caveats that you must understand before using them.
Non-deterministic timing. You cannot rely on when (or even if) a finalization callback will run. The garbage collector decides when to collect objects. Callbacks may be batched, delayed, or in some edge cases, never called (for example, if the page is closed before GC runs).
Do not use for correctness. FinalizationRegistry is a safety net, not a primary cleanup mechanism. Always provide explicit cleanup methods (like close() or dispose()) and use finalization only as a fallback.
Performance implications. Registering many objects with a FinalizationRegistry has overhead. The garbage collector must track registered objects and schedule callbacks.
WeakRef consistency. Within a single synchronous execution, deref() returns consistent results. But across event loop turns, the object may be collected between calls.
Summary
WeakRef and FinalizationRegistry provide controlled interaction with JavaScript’s garbage collector:
WeakRefholds a reference that does not prevent garbage collection. Use.deref()to access the object, which returnsundefinedafter collection.FinalizationRegistryruns a callback when a registered object is collected, enabling automatic resource cleanup.- Combining both enables self-cleaning caches that shrink under memory pressure.
- These APIs are safety nets, not primary cleanup mechanisms. Always provide explicit disposal methods.
- Garbage collection timing is non-deterministic. Never rely on finalization for program correctness.
Related articles
- Python Python Weak References and Garbage Collection
Understand Python's garbage collection, reference counting, and how weak references prevent memory leaks in caches, observers, and circular structures.
- Node.js Debugging Node.js Memory Leaks
Find and fix memory leaks in Node.js using heap snapshots, sampling, and a few reliable patterns to avoid leaks.
- JavaScript JavaScript Event Loop Explained: How Async Code Really Works
Understand how the JavaScript event loop handles async operations including the call stack, microtasks, macrotasks, and execution order with practical examples.
- JavaScript JavaScript structuredClone: The Modern Deep Copy Solution
Learn how to use JavaScript structuredClone for deep copying objects, when it beats JSON.parse(JSON.stringify()), and what types it supports and cannot handle.