JavaScript WeakRef and FinalizationRegistry
JavaScript's garbage collector automatically reclaims memory from objects that are no longer reachable. Normally, any variable holding a reference to an object keeps that object alive—this is a...
Key Insights
- WeakRef allows you to hold references to objects without preventing garbage collection, solving memory leak issues in caches and observer patterns where strong references would keep objects alive indefinitely
- FinalizationRegistry enables cleanup callbacks when objects are garbage collected, but should never be used for critical application logic due to non-deterministic timing
- These APIs are powerful for memory-sensitive applications but come with significant caveats—most developers should stick with WeakMap and WeakSet for typical use cases
Understanding Memory References in JavaScript
JavaScript’s garbage collector automatically reclaims memory from objects that are no longer reachable. Normally, any variable holding a reference to an object keeps that object alive—this is a “strong reference.” The problem arises when you want to reference an object without preventing its cleanup.
Consider a cache that stores expensive-to-compute results. With strong references, cached objects stay in memory forever, even when nothing else needs them. Before WeakRef, your only option was WeakMap, which works well for key-value scenarios but doesn’t cover all use cases.
// Strong reference - prevents garbage collection
const cache = new Map();
let largeObject = { data: new Array(1000000) };
cache.set('key', largeObject);
largeObject = null; // Object still in memory via cache
// Weak reference - allows garbage collection
const weakCache = new WeakMap();
let largeObject2 = { data: new Array(1000000) };
weakCache.set(largeObject2, 'computed-value');
largeObject2 = null; // Object can be garbage collected
WeakRef and FinalizationRegistry give you fine-grained control over these scenarios, but with complexity and gotchas you need to understand.
The WeakRef API
WeakRef creates a weak reference to an object. The object can be garbage collected even though you hold a WeakRef to it. You access the object through the deref() method, which returns the object if it’s still alive or undefined if it’s been collected.
let target = { name: 'Important Data', value: 42 };
const weakRef = new WeakRef(target);
// Access the object
console.log(weakRef.deref()); // { name: 'Important Data', value: 42 }
// Remove strong reference
target = null;
// At some point after GC runs...
console.log(weakRef.deref()); // undefined (maybe - GC timing is unpredictable)
The critical detail: deref() might return undefined at any time after the last strong reference disappears. You must always check the return value.
Here’s a practical cache implementation using WeakRef:
class WeakRefCache {
constructor() {
this.cache = new Map();
}
set(key, value) {
this.cache.set(key, new WeakRef(value));
}
get(key) {
const weakRef = this.cache.get(key);
if (!weakRef) return undefined;
const value = weakRef.deref();
if (value === undefined) {
// Object was garbage collected, clean up the entry
this.cache.delete(key);
}
return value;
}
has(key) {
return this.get(key) !== undefined;
}
}
// Usage
const cache = new WeakRefCache();
let expensiveData = { result: computeExpensiveOperation() };
cache.set('computation-1', expensiveData);
// Later...
const cached = cache.get('computation-1');
if (cached) {
console.log('Cache hit:', cached.result);
} else {
console.log('Cache miss - recomputing...');
}
This cache allows objects to be garbage collected when memory pressure increases, while still providing fast access when they’re available. The key advantage over WeakMap is that you can use primitive keys like strings.
FinalizationRegistry Deep Dive
FinalizationRegistry lets you register a callback that runs after an object is garbage collected. This is useful for cleanup tasks like closing file handles, releasing native resources, or updating statistics.
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Object with id ${heldValue} was garbage collected`);
});
let obj = { data: 'important' };
// Register the object with a "held value" that identifies it
registry.register(obj, 'object-1');
obj = null; // Remove strong reference
// Eventually, the callback will run with 'object-1'
The held value (second argument) is what gets passed to your callback. It should be a primitive or something that won’t prevent garbage collection of the target object.
For more control, you can provide an unregister token:
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Cleanup: ${heldValue}`);
});
let resource = { handle: 123 };
const unregisterToken = { id: 'token-1' };
registry.register(resource, 'resource-123', unregisterToken);
// If you want to cancel the finalization callback
registry.unregister(unregisterToken);
resource = null; // Callback won't run
Real-World Applications
Image Cache with Automatic Cleanup
class ImageCache {
constructor() {
this.cache = new Map();
this.registry = new FinalizationRegistry((url) => {
console.log(`Image ${url} was garbage collected`);
this.cache.delete(url);
});
}
async loadImage(url) {
// Check if we have a weak reference
const weakRef = this.cache.get(url);
if (weakRef) {
const img = weakRef.deref();
if (img) return img;
}
// Load the image
const img = new Image();
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = url;
});
// Cache with weak reference
this.cache.set(url, new WeakRef(img));
this.registry.register(img, url);
return img;
}
}
Resource Pool with Cleanup Tracking
class ResourcePool {
constructor(createResource, destroyResource) {
this.createResource = createResource;
this.destroyResource = destroyResource;
this.activeResources = new Set();
this.registry = new FinalizationRegistry((resourceId) => {
console.warn(`Resource ${resourceId} was GC'd without explicit release`);
this.destroyResource(resourceId);
});
}
acquire() {
const resource = this.createResource();
const id = Math.random().toString(36);
this.activeResources.add(id);
this.registry.register(resource, id, resource);
return resource;
}
release(resource) {
// Proper cleanup - unregister to prevent finalization callback
this.registry.unregister(resource);
this.destroyResource(resource.id);
}
}
// Usage
const pool = new ResourcePool(
() => ({ id: Date.now(), connection: openDatabaseConnection() }),
(id) => closeDatabaseConnection(id)
);
const resource = pool.acquire();
// Use resource...
pool.release(resource); // Explicit cleanup
Critical Caveats and Anti-Patterns
The biggest mistake developers make is treating FinalizationRegistry callbacks as reliable cleanup mechanisms. They’re not. Garbage collection timing is completely unpredictable and implementation-dependent.
Never do this:
// ANTI-PATTERN: Don't rely on finalization for critical cleanup
const registry = new FinalizationRegistry((fileHandle) => {
// This might run immediately, in 5 minutes, or never!
closeFile(fileHandle); // Critical resource leak if this doesn't run
});
function processFile(path) {
const file = openFile(path);
registry.register(file, file.handle);
// Missing explicit file.close() - WRONG!
return file.read();
}
Do this instead:
// CORRECT: Explicit cleanup with finalization as backup
function processFile(path) {
const file = openFile(path);
try {
return file.read();
} finally {
file.close(); // Always clean up explicitly
}
}
FinalizationRegistry should be a safety net for debugging or handling unexpected scenarios, not your primary cleanup mechanism.
Performance Considerations
WeakRef and FinalizationRegistry have overhead. Creating WeakRefs isn’t free, and the garbage collector needs to do extra bookkeeping. For most applications, WeakMap and WeakSet are faster and simpler:
// Prefer WeakMap when keys are objects
const metadata = new WeakMap();
metadata.set(userObject, { lastAccess: Date.now() });
// Use WeakRef when you need primitive keys or more control
const cache = new Map();
cache.set('user-123', new WeakRef(userObject));
When to Use These APIs
Use WeakRef when:
- Building caches with primitive keys where entries should be collectible
- Implementing observer patterns that shouldn’t prevent object cleanup
- Creating memory-sensitive data structures in long-running applications
Use FinalizationRegistry when:
- Debugging memory leaks (logging when objects are collected)
- Implementing safety-net cleanup for native resources
- Tracking object lifecycles in development environments
Avoid them when:
- You need reliable, deterministic cleanup (use explicit cleanup instead)
- WeakMap or WeakSet would work (they’re simpler and faster)
- You’re building typical web applications (the complexity usually isn’t worth it)
Wrapping Up
WeakRef and FinalizationRegistry are advanced tools that solve specific memory management problems. They’re most valuable in long-running applications, complex caching scenarios, or when interfacing with native resources. For typical web development, WeakMap and WeakSet handle 95% of weak reference needs with less complexity.
The key takeaway: these APIs give you power, but with significant responsibility. Garbage collection is non-deterministic, and you cannot rely on timing. Use them to optimize memory usage and as safety nets, but never for critical application logic. When in doubt, explicit resource management with try-finally blocks is always the safer choice.