WeakRef & FinalizationRegistry
WeakRef (ES2021) and FinalizationRegistry (ES2021) are low-level JavaScript primitives that give developers direct access to garbage collection behavior. A WeakRef holds a weak reference to an object — one that does not prevent the garbage collector from reclaiming the object. FinalizationRegistry lets you register callbacks that fire when an object is garbage collected.
These primitives enable patterns like memory-efficient caches, resource cleanup without manual disposal, and memory-sensitive applications. However, they come with significant caveats: garbage collection timing is non-deterministic, callbacks may never fire, and overuse can lead to subtle bugs. Use them for optimization, never for correctness.
| 1 | // WeakRef basics |
| 2 | let target = { data: "important" }; |
| 3 | const ref = new WeakRef(target); |
| 4 | |
| 5 | // Access the referenced object |
| 6 | console.log(ref.deref()); // { data: "important" } |
| 7 | |
| 8 | // Remove strong reference |
| 9 | target = null; |
| 10 | |
| 11 | // At some later point (non-deterministic): |
| 12 | // ref.deref() may return the object or undefined |
| 13 | // depending on whether GC has run |
A weak reference is a reference that does not prevent garbage collection. If an object is only reachable through weak references, the garbage collector can reclaim it at any time. This is different from strong references, which keep objects alive as long as they exist.
Think of it like this: a strong reference is a lease on an apartment — the tenant (object) stays as long as the lease (reference) exists. A weak reference is like knowing the address — you can check if someone still lives there, but you cannot prevent them from moving out.
| 1 | // Strong vs Weak references |
| 2 | let strong = { value: 42 }; |
| 3 | let weak = new WeakRef(strong); |
| 4 | |
| 5 | console.log(strong); // { value: 42 } |
| 6 | console.log(weak.deref()); // { value: 42 } — same object |
| 7 | |
| 8 | // Strong reference keeps object alive |
| 9 | strong = null; |
| 10 | // Object still alive — 'weak' still holds a reference |
| 11 | console.log(weak.deref()); // might still return { value: 42 } |
| 12 | |
| 13 | // When GC runs (non-deterministic): |
| 14 | // - If no strong references remain, object is collected |
| 15 | // - weak.deref() then returns undefined |
| 16 | |
| 17 | // Practical: understanding reachability |
| 18 | function createObject() { |
| 19 | const obj = { data: "temporary" }; |
| 20 | const ref = new WeakRef(obj); |
| 21 | // obj has a strong reference within this scope |
| 22 | return ref; |
| 23 | // After return, 'obj' goes out of scope |
| 24 | // Only 'ref' (weak reference) remains |
| 25 | // Object is eligible for GC |
| 26 | } |
| 27 | |
| 28 | const ref = createObject(); |
| 29 | console.log(ref.deref()); // May return { data: "temporary" } or undefined |
| 30 | |
| 31 | // The key insight: |
| 32 | // Strong reference = object stays alive |
| 33 | // Weak reference = object may be collected at any time |
| 34 | // GC timing is engine-specific and non-deterministic |
info
The deref() method returns the referenced object if it is still alive, or undefined if it has been garbage collected. Always use optional chaining or nullish coalescing when accessing deref results.
| 1 | // deref() usage patterns |
| 2 | let obj = { name: "test" }; |
| 3 | const ref = new WeakRef(obj); |
| 4 | |
| 5 | // Pattern 1: Optional chaining |
| 6 | const data = ref.deref()?.name; // "test" or undefined |
| 7 | |
| 8 | // Pattern 2: Nullish coalescing |
| 9 | const value = ref.deref() ?? "default"; // object or "default" |
| 10 | |
| 11 | // Pattern 3: Check before use |
| 12 | if (ref.deref() !== undefined) { |
| 13 | console.log("Object is alive"); |
| 14 | } |
| 15 | |
| 16 | // Pattern 4: Safe access with fallback |
| 17 | function getData(ref, fallback) { |
| 18 | const obj = ref.deref(); |
| 19 | if (obj === undefined) { |
| 20 | console.log("Object was collected, using fallback"); |
| 21 | return fallback; |
| 22 | } |
| 23 | return obj; |
| 24 | } |
| 25 | |
| 26 | // Practical: image cache with deref |
| 27 | const imageCache = new Map(); |
| 28 | |
| 29 | function getCachedImage(url) { |
| 30 | const ref = imageCache.get(url); |
| 31 | if (ref) { |
| 32 | const img = ref.deref(); |
| 33 | if (img) { |
| 34 | console.log("Cache hit"); |
| 35 | return img; |
| 36 | } |
| 37 | // Image was collected — clean up stale entry |
| 38 | imageCache.delete(url); |
| 39 | } |
| 40 | console.log("Cache miss — loading image"); |
| 41 | return loadImage(url).then(img => { |
| 42 | imageCache.set(url, new WeakRef(img)); |
| 43 | return img; |
| 44 | }); |
| 45 | } |
| 46 | |
| 47 | // Practical: DOM node access |
| 48 | function safeGetNode(ref) { |
| 49 | const node = ref.deref(); |
| 50 | if (!node || !node.parentNode) { |
| 51 | return null; // Node was removed from DOM |
| 52 | } |
| 53 | return node; |
| 54 | } |
FinalizationRegistrylets you register a callback that is invoked when an object is garbage collected. The callback receives a "held value" that you provide at registration time. This is useful for cleaning up external resources (file handles, network connections, native memory) when the JavaScript object is no longer reachable.
| 1 | // FinalizationRegistry basics |
| 2 | const registry = new FinalizationRegistry((heldValue) => { |
| 3 | console.log("Object collected, cleaning up:", heldValue); |
| 4 | }); |
| 5 | |
| 6 | function createResource(id) { |
| 7 | const resource = { id, data: new Array(1000000) }; |
| 8 | registry.register(resource, "resource-" + id); |
| 9 | return resource; |
| 10 | } |
| 11 | |
| 12 | // Register objects for cleanup |
| 13 | let res1 = createResource(1); |
| 14 | let res2 = createResource(2); |
| 15 | |
| 16 | // When res1/res2 go out of scope and are GC'd, |
| 17 | // the callback fires with the held value |
| 18 | |
| 19 | res1 = null; // res1 is eligible for GC |
| 20 | // At some point: "Object collected, cleaning up: resource-1" |
| 21 | |
| 22 | res2 = null; // res2 is eligible for GC |
| 23 | // At some point: "Object collected, cleaning up: resource-2" |
| 24 | |
| 25 | // Unregister if you want to cancel cleanup |
| 26 | const res3 = createResource(3); |
| 27 | registry.unregister(res3); // Cleanup callback will NOT fire for res3 |
| 1 | // Practical: cleaning up native resources |
| 2 | class ManagedConnection { |
| 3 | constructor(url) { |
| 4 | this.url = url; |
| 5 | this.socket = connectToServer(url); |
| 6 | |
| 7 | // Register cleanup — if user forgets to close() |
| 8 | this._registry = new FinalizationRegistry((socket) => { |
| 9 | console.log("Cleaning up connection to", url); |
| 10 | socket.close(); |
| 11 | }); |
| 12 | this._registry.register(this, this.socket); |
| 13 | } |
| 14 | |
| 15 | send(data) { |
| 16 | this.socket.send(data); |
| 17 | } |
| 18 | |
| 19 | close() { |
| 20 | if (this.socket) { |
| 21 | this.socket.close(); |
| 22 | this.socket = null; |
| 23 | this._registry.unregister(this); |
| 24 | } |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | // Usage |
| 29 | function processConnection() { |
| 30 | const conn = new ManagedConnection("wss://api.example.com"); |
| 31 | conn.send("hello"); |
| 32 | // If processConnection returns and conn is not stored, |
| 33 | // the finalizer closes the socket automatically |
| 34 | } |
| 35 | // But if you store the connection, it stays alive: |
| 36 | const connection = new ManagedConnection("wss://api.example.com"); |
| 37 | // Must call connection.close() explicitly — finalizer won't fire while alive |
danger
WeakRef and FinalizationRegistry are best used for optimization and safety-net patterns. They shine in caches, connection pools, and memory-sensitive applications where retaining objects indefinitely would waste memory.
| 1 | // Use Case 1: Weak cache that auto-evicts |
| 2 | function createWeakCache(computeFn) { |
| 3 | const cache = new Map(); |
| 4 | |
| 5 | return function(key) { |
| 6 | const ref = cache.get(key); |
| 7 | if (ref) { |
| 8 | const cached = ref.deref(); |
| 9 | if (cached !== undefined) { |
| 10 | return cached; |
| 11 | } |
| 12 | cache.delete(key); // Clean up stale entry |
| 13 | } |
| 14 | |
| 15 | const result = computeFn(key); |
| 16 | cache.set(key, new WeakRef(result)); |
| 17 | return result; |
| 18 | }; |
| 19 | } |
| 20 | |
| 21 | // Use Case 2: Object pool with auto-cleanup |
| 22 | class ObjectPool { |
| 23 | constructor(Factory, maxPoolSize = 100) { |
| 24 | this.pool = new Set(); |
| 25 | this.Factory = Factory; |
| 26 | this.maxPoolSize = maxPoolSize; |
| 27 | |
| 28 | this._registry = new FinalizationRegistry((obj) => { |
| 29 | this.pool.delete(obj); |
| 30 | }); |
| 31 | } |
| 32 | |
| 33 | acquire() { |
| 34 | // Try to reuse a collected object's slot |
| 35 | const obj = new this.Factory(); |
| 36 | if (this.pool.size < this.maxPoolSize) { |
| 37 | this.pool.add(obj); |
| 38 | this._registry.register(obj, obj); |
| 39 | } |
| 40 | return obj; |
| 41 | } |
| 42 | |
| 43 | release(obj) { |
| 44 | this.pool.delete(obj); |
| 45 | this._registry.unregister(obj); |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | // Use Case 3: DOM observer that auto-cleans |
| 50 | function observeElement(el, callback) { |
| 51 | const observer = new MutationObserver(callback); |
| 52 | observer.observe(el, { childList: true, subtree: true }); |
| 53 | |
| 54 | // Auto-disconnect when element is GC'd |
| 55 | const registry = new FinalizationRegistry(() => { |
| 56 | observer.disconnect(); |
| 57 | }); |
| 58 | registry.register(el, observer); |
| 59 | |
| 60 | return observer; |
| 61 | } |
Understanding garbage collection behavior is essential for using WeakRef and FinalizationRegistry correctly. GC timing varies between engines and is influenced by memory pressure, heap size, and engine optimizations.
| 1 | // GC behavior demonstration |
| 2 | function demonstrateGC() { |
| 3 | let obj = { data: "important" }; |
| 4 | const ref = new WeakRef(obj); |
| 5 | const registry = new FinalizationRegistry((heldValue) => { |
| 6 | console.log("Finalized:", heldValue); |
| 7 | }); |
| 8 | |
| 9 | registry.register(obj, "my-data"); |
| 10 | |
| 11 | // Object is strongly referenced by 'obj' |
| 12 | console.log(ref.deref()); // Object alive |
| 13 | |
| 14 | // Remove strong reference |
| 15 | obj = null; |
| 16 | // Object is NOW eligible for GC |
| 17 | // But GC may not run immediately |
| 18 | |
| 19 | // Force GC (V8/Node.js only — not available in browsers) |
| 20 | if (globalThis.gc) { |
| 21 | globalThis.gc(); |
| 22 | // Now ref.deref() will likely return undefined |
| 23 | // And the finalization callback will fire |
| 24 | } |
| 25 | |
| 26 | console.log(ref.deref()); // May be undefined or still the object |
| 27 | } |
| 28 | |
| 29 | // Important: FinalizationRegistry callbacks fire in microtask queue |
| 30 | // They are NOT synchronous with GC |
| 31 | // The object may be collected but the callback hasn't fired yet |
| 32 | |
| 33 | // What you CAN rely on: |
| 34 | // - If deref() returns undefined, the object was collected |
| 35 | // - If deref() returns an object, it is still alive (for now) |
| 36 | // - FinalizationRegistry callback fires AFTER deref() returns undefined |
| 37 | |
| 38 | // What you CANNOT rely on: |
| 39 | // - WHEN GC will run |
| 40 | // - WHETHER GC will run at all during your program's lifetime |
| 41 | // - WHEN the finalization callback will fire after collection |
| 42 | // - That the callback will fire before the program exits |
warning