|$ curl https://forge-ai.dev/api/markdown?path=docs/js/weakref
$cat docs/weakref-&-finalizationregistry.md
updated Last week·22 min read·published

WeakRef & FinalizationRegistry

JavaScriptMemoryAdvancedAdvanced🎯Free Tools
Introduction

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.

weakref-intro.js
JavaScript
1// WeakRef basics
2let target = { data: "important" };
3const ref = new WeakRef(target);
4
5// Access the referenced object
6console.log(ref.deref()); // { data: "important" }
7
8// Remove strong reference
9target = 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
WeakRef Concept

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.

weakref-concept.js
JavaScript
1// Strong vs Weak references
2let strong = { value: 42 };
3let weak = new WeakRef(strong);
4
5console.log(strong); // { value: 42 }
6console.log(weak.deref()); // { value: 42 } — same object
7
8// Strong reference keeps object alive
9strong = null;
10// Object still alive — 'weak' still holds a reference
11console.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
18function 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
28const ref = createObject();
29console.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 garbage collector may never run during your program's lifetime, or it may run at any time. ref.deref() might return the object immediately after creating a WeakRef, or it might return undefined right away. Never depend on timing.
deref() Method

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.

weakref-deref.js
JavaScript
1// deref() usage patterns
2let obj = { name: "test" };
3const ref = new WeakRef(obj);
4
5// Pattern 1: Optional chaining
6const data = ref.deref()?.name; // "test" or undefined
7
8// Pattern 2: Nullish coalescing
9const value = ref.deref() ?? "default"; // object or "default"
10
11// Pattern 3: Check before use
12if (ref.deref() !== undefined) {
13 console.log("Object is alive");
14}
15
16// Pattern 4: Safe access with fallback
17function 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
27const imageCache = new Map();
28
29function 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
48function 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}
FinalizationRegistry

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.

finalization-basics.js
JavaScript
1// FinalizationRegistry basics
2const registry = new FinalizationRegistry((heldValue) => {
3 console.log("Object collected, cleaning up:", heldValue);
4});
5
6function 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
13let res1 = createResource(1);
14let 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
19res1 = null; // res1 is eligible for GC
20// At some point: "Object collected, cleaning up: resource-1"
21
22res2 = 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
26const res3 = createResource(3);
27registry.unregister(res3); // Cleanup callback will NOT fire for res3
finalization-practical.js
JavaScript
1// Practical: cleaning up native resources
2class 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
29function 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:
36const connection = new ManagedConnection("wss://api.example.com");
37// Must call connection.close() explicitly — finalizer won't fire while alive

danger

FinalizationRegistry callbacks are not guaranteedto run. They run in the microtask queue and may be delayed indefinitely or never called during your program's lifetime. Never rely on them for correctness-critical operations like flushing buffers or releasing locks. Always provide explicit cleanup methods and treat the finalizer as a safety net only.
Use Cases

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.

weakref-use-cases.js
JavaScript
1// Use Case 1: Weak cache that auto-evicts
2function 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
22class 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
50function 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}
GC Behavior

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.

weakref-gc-behavior.js
JavaScript
1// GC behavior demonstration
2function 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

Never depend on garbage collection timing. Your code must work correctly whether the object is collected or not. WeakRef is for optimization— making caches memory-efficient, cleaning up resources when objects are no longer needed. If your code's correctness depends on the object being alive, you need a strong reference, not a WeakRef.
Best Practices
Use WeakRef for optimization only — never for correctness-critical data
Always check deref() with optional chaining or nullish coalescing
Provide explicit cleanup methods alongside FinalizationRegistry
Never rely on FinalizationRegistry callbacks for critical operations
Use WeakRef in caches to allow GC of unused cached objects
Combine WeakRef with Map for caches that track stale entries
Test with globalThis.gc() in Node.js to verify cleanup behavior
Consider WeakMap/WeakSet before WeakRef — they handle common patterns more simply
$Blueprint — Engineering Documentation·Section ID: JS-WEAKREF·Revision: 1.0