What are WeakRef and FinalizationRegistry used for?
Assesses fundamental understanding of JavaScript conventions, runtime behavior, and memory/performance considerations.
Hiring managers look for precision, avoidance of ambiguous jargon, and ability to explain trade-offs under real production conditions.
WeakRef and FinalizationRegistry are advanced features (introduced in ES2021) for managing memory and object lifecycles in JavaScript. They provide low-level control over garbage collection behavior.
WeakRef (Weak Reference):
A WeakRef creates a weak reference to an object, meaning it doesn't prevent the object from being garbage collected. Unlike regular references, holding a WeakRef doesn't keep the object alive.
Syntax:
const weakRef = new WeakRef(targetObject);
const obj = weakRef.deref(); // Get the object (or undefined if collected)
WeakRef Examples:
// Creating a weak reference
let obj = { name: 'Important Data', value: 42 };
const weakRef = new WeakRef(obj);
// Access the object
console.log(weakRef.deref()); // { name: 'Important Data', value: 42 }
// Remove strong reference
obj = null;
// At some point, after garbage collection
console.log(weakRef.deref()); // undefined (object was collected)
// Practical use: Caching without memory leaks
class ImageCache {
#cache = new Map();
getImage(url) {
const weakRef = this.#cache.get(url);
if (weakRef) {
const image = weakRef.deref();
if (image) {
console.log('Cache hit!');
return image;
}
}
// Load image if not in cache or was collected
console.log('Cache miss, loading...');
const newImage = this.loadImage(url);
this.#cache.set(url, new WeakRef(newImage));
return newImage;
}
loadImage(url) {
// Simulate loading
return { url, data: `Image data for ${url}` };
}
}
const cache = new ImageCache();
const img1 = cache.getImage('photo.jpg'); // Cache miss
const img2 = cache.getImage('photo.jpg'); // Cache hit!
FinalizationRegistry:
FinalizationRegistry allows you to register callbacks that run after objects are garbage collected. This enables cleanup actions when objects are no longer needed.
Syntax:
const registry = new FinalizationRegistry((heldValue) => {
// Cleanup callback when object is garbage collected
console.log('Cleaning up:', heldValue);
});
registry.register(targetObject, heldValue, unregisterToken);
FinalizationRegistry Examples:
// Basic usage
const registry = new FinalizationRegistry((filename) => {
console.log(`File ${filename} can be deleted - object was collected`);
// Perform cleanup: close file handles, free resources, etc.
});
let fileObject = { name: 'temp.txt', handle: 'handle123' };
registry.register(fileObject, 'temp.txt');
// When fileObject is garbage collected, the callback runs
fileObject = null; // Remove strong reference
// Real-world example: Resource management
class FileManager {
#registry = new FinalizationRegistry((filepath) => {
this.#closeFile(filepath);
});
#openFiles = new Map();
openFile(filepath) {
const handle = this.#actuallyOpenFile(filepath);
const file = { filepath, handle };
this.#openFiles.set(filepath, handle);
this.#registry.register(file, filepath);
return file;
}
#actuallyOpenFile(filepath) {
console.log(`Opening ${filepath}`);
return { /* file handle */ };
}
#closeFile(filepath) {
const handle = this.#openFiles.get(filepath);
if (handle) {
console.log(`Auto-closing ${filepath}`);
// Close file handle
this.#openFiles.delete(filepath);
}
}
}
// Database connection pooling
class ConnectionPool {
#registry = new FinalizationRegistry((connectionId) => {
console.log(`Connection ${connectionId} released`);
this.#releaseConnection(connectionId);
});
#connections = new Map();
getConnection() {
const connectionId = Math.random().toString(36);
const connection = { id: connectionId, query: () => {} };
this.#connections.set(connectionId, connection);
this.#registry.register(connection, connectionId);
return connection;
}
#releaseConnection(connectionId) {
this.#connections.delete(connectionId);
// Return connection to pool
}
}
// Using unregister token to prevent cleanup
const cleanupRegistry = new FinalizationRegistry((msg) => {
console.log('Cleanup:', msg);
});
let importantObj = { data: 'important' };
const token = {}; // Unregister token
cleanupRegistry.register(importantObj, 'important data', token);
// Later, if you want to prevent cleanup
cleanupRegistry.unregister(token); // Callback won't run even after GC
Combined Example - Cache with Cleanup:
class SmartCache {
#cache = new Map();
#registry = new FinalizationRegistry((key) => {
console.log(`Removing cache entry: ${key}`);
this.#cache.delete(key);
});
set(key, value) {
const weakRef = new WeakRef(value);
this.#cache.set(key, weakRef);
this.#registry.register(value, key, weakRef);
}
get(key) {
const weakRef = this.#cache.get(key);
if (!weakRef) return undefined;
const value = weakRef.deref();
if (value === undefined) {
// Object was collected, clean up map
this.#cache.delete(key);
}
return value;
}
has(key) {
return this.get(key) !== undefined;
}
delete(key) {
const weakRef = this.#cache.get(key);
if (weakRef) {
this.#registry.unregister(weakRef);
this.#cache.delete(key);
}
}
}
const cache = new SmartCache();
let data = { huge: 'dataset' };
cache.set('myData', data);
console.log(cache.get('myData')); // { huge: 'dataset' }
data = null; // Remove strong reference
// After GC, cache entry is automatically cleaned up
Important Caveats:
- Non-Deterministic: Garbage collection timing is unpredictable
- No Guarantees: The finalization callback may never run (e.g., if the process exits)
- Performance: These are advanced features; use only when necessary
- Avoid Over-Use: Regular JavaScript patterns are usually better
- Not for Critical Logic: Don't rely on finalization for business logic
When to Use:
- ✅ Caching large objects that can be recreated
- ✅ Managing native resources (file handles, sockets)
- ✅ Automatic cleanup of external resources
- ✅ Memory-sensitive applications
- ❌ Not for regular object lifecycle management
- ❌ Not for critical cleanup (use explicit cleanup instead)
Candidate Response Strategy & Interview Tips
- Start with a concise one-sentence summary: Deliver a direct, confident answer first before expanding into nuances.
- Demonstrate real-world trade-offs: Discuss where this approach excels and when you would avoid it in production systems.
- Discuss complexity & edge cases: Proactively explain time/space complexity or boundary conditions (null values, scale limits).
- Prepare for interviewer follow-ups: Technical hiring panels frequently probe deeper into concurrency, backward compatibility, or alternative libraries.