Suggest an editImprove this articleRefine the answer for “WeakRef and finalizationregistry in JavaScript”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**WeakRef** holds a reference to an object without preventing garbage collection. **FinalizationRegistry** runs a callback when a registered object is collected. ```javascript let obj = { id: 1 }; const weakRef = new WeakRef(obj); obj = null; // Object may be GC'd console.log(weakRef.deref()?.id); // 1 or undefined const registry = new FinalizationRegistry((key) => { console.log(`${key} was collected`); }); registry.register(obj, "obj-key"); ``` **Key point:** FinalizationRegistry callback timing is not guaranteed. Use both features for caches and optional cleanup only - never for critical resource management.Shown above the full answer for quick recall.Answer (EN)Image**WeakRef** holds a reference to an object without preventing garbage collection. **FinalizationRegistry** runs a callback when that object is actually collected. ## Theory ### TL;DR - WeakRef is like a sticky note with someone's number: it doesn't keep them in town, but you can check if they're still around - `.deref()` returns the object if it's still alive, or `undefined` if it's been collected - FinalizationRegistry fires a callback after GC collects the registered object - timing is not guaranteed and may be delayed by seconds or more - Use these for caches, observer cleanup, or optional resource tracking where you want objects to be collectable - Never use FinalizationRegistry for time-critical cleanup like closing file handles or releasing locks ### Quick example ```javascript // Normal reference keeps object alive let obj = { id: 1 }; const ref = obj; obj = null; console.log(ref.id); // 1 - still alive, ref is a strong reference // WeakRef does not prevent garbage collection let obj2 = { id: 2 }; const weakRef = new WeakRef(obj2); obj2 = null; // obj2 is now eligible for collection console.log(weakRef.deref()?.id); // 2 if still alive, undefined if collected ``` The difference is in how the GC counts references. Strong references are counted. WeakRef is not. Once all strong references are gone, the object is eligible for collection no matter how many WeakRefs point at it. ### Key difference The garbage collector tracks strong references to decide what to keep alive. WeakRef does not add to that count. When you call `.deref()`, you either get the object or `undefined` - you cannot know in advance which one. FinalizationRegistry watches for the collection event and schedules a callback, but that callback runs during a future GC cycle, not at the moment the last strong reference disappears. ### When to use - **WeakRef:** caches where entries should disappear once the original object is no longer used elsewhere; observer patterns where listeners should not keep subjects alive; DOM node tracking in framework internals - **FinalizationRegistry:** logging when cache entries are evicted by GC; releasing native resources in Node.js add-on bindings; cleaning up subscriptions in observable libraries when explicit unsubscribe is missing ### How the engine handles this V8, SpiderMonkey, and JavaScriptCore all maintain a separate weak reference table that does not contribute to reachability. During a GC pass, if an object has no strong references, it gets marked for collection regardless of any WeakRefs pointing at it. After the object is freed, the engine queues FinalizationRegistry callbacks, but the exact timing depends on GC cycles, heap pressure, and the runtime environment. I've seen teams waste hours debugging cleanup code that assumed these callbacks behaved like destructors - they don't. ### Common mistakes **Mistake 1: Assuming FinalizationRegistry callbacks run immediately** An object can be unreachable for seconds or more before the callback fires. Some GC implementations may defer it across many cycles. ```javascript // Wrong - relies on callback for timely resource cleanup const registry = new FinalizationRegistry(() => { fileHandle.close(); // May not run for a long time }); registry.register(fileStream, fileHandle); // Right - explicit cleanup, guaranteed and immediate class FileStream { close() { this.fileHandle.close(); } } stream.close(); ``` **Mistake 2: Passing the object itself as the held value** ```javascript // Wrong - creates a strong reference inside the registry, prevents collection registry.register(obj, obj); // Right - pass only an identifier or metadata registry.register(obj, obj.id); ``` **Mistake 3: Not handling undefined from .deref()** ```javascript // Wrong - crashes with TypeError if object was collected const config = new WeakRef(globalConfig); const value = config.deref().setting; // Right const target = config.deref(); const value = target?.setting; // safely undefined if collected ``` **Mistake 4: Using WeakRef with primitives** ```javascript // Wrong - throws TypeError new WeakRef(42); new WeakRef("hello"); // Right - objects only new WeakRef({ value: 42 }); new WeakRef([1, 2, 3]); ``` Primitives are immutable and interned by the engine. There is no object identity to track, so weak references to them make no sense. **Mistake 5: Confusing WeakRef with WeakMap** WeakMap uses weak keys: you associate data with objects, and the entire entry disappears automatically when the key is collected. WeakRef is an explicit handle you check manually with `.deref()`. Use WeakMap for "metadata about objects"; use WeakRef for "is this specific object still alive?" ```javascript // WeakMap - entry disappears automatically when key is collected const wm = new WeakMap(); let user = { id: 1 }; wm.set(user, { role: "admin" }); user = null; // entry gone on next GC // WeakRef - you control the check const wr = new WeakRef(user); const alive = wr.deref(); // you decide what to do with undefined ``` ### Real-world usage - React: tracking DOM nodes in refs without blocking their collection; FinalizationRegistry in cleanup for native bindings - Node.js streams: detecting when stream objects are dereferenced to trigger cleanup callbacks - Electron: managing native window handles to prevent memory leaks when JS objects outlive closed windows - Web Workers: auto-removing dead workers from a pool via FinalizationRegistry callbacks - RxJS and similar observable libraries: cleaning up subscriptions when observers are collected without an explicit `unsubscribe()` call ### Follow-up questions **Q:** Why can't WeakRef work with primitives like numbers or strings? **A:** Primitives are immutable and interned by the engine - there is no meaningful object identity to track. Only objects have stable identities that the GC can monitor and collect. **Q:** If FinalizationRegistry callbacks aren't guaranteed to run, when should you actually use it? **A:** Use it for non-critical work: logging, metrics, cache invalidation, or releasing resources that have fallbacks. Never use it where correctness depends on timely cleanup. **Q:** Can you create a memory leak with WeakRef? **A:** Yes. If you store the `.deref()` result in a strong reference and never clear it, you've kept the object alive anyway. Also, if your FinalizationRegistry callback holds a reference to the collected object through a closure, you create a cycle that blocks collection. **Q:** What is the difference between WeakRef and WeakMap, and when would you use each? **A:** WeakMap pairs data with an object as a key - when the key is collected, the entry disappears automatically. WeakRef is an explicit handle where you call `.deref()` yourself to check. WeakMap for "metadata about objects"; WeakRef for "is this object still alive?" **Q (Senior):** Design a cache that auto-evicts entries when values are collected but also enforces a maximum size. What edge cases matter? **A:** Use WeakRef for values and FinalizationRegistry for cleanup callbacks, plus an LRU list of strong references for size capping. Key edge cases: the callback may arrive after the cache is destroyed; holding the cache itself in a closure can block GC of cached objects; the callback may fire after you've already replaced an entry under the same key so check before deleting; the size limit is a soft guarantee, not a hard one. ## Examples ### Basic: WeakRef in a simple cache ```javascript class SimpleWeakCache { #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) { this.#cache.delete(key); // Remove stale entry } return value; } } const cache = new SimpleWeakCache(); let user = { name: "Alice" }; cache.set("alice", user); console.log(cache.get("alice")); // { name: "Alice" } user = null; // Remove strong reference // After next GC cycle: cache.get("alice") returns undefined ``` The `get` method always checks `.deref()` before returning. If the object was collected, it removes the stale Map entry so the cache doesn't grow unboundedly. ### Intermediate: Auto-cleaning cache with FinalizationRegistry ```javascript class AutoCache { #cache = new Map(); #registry = new FinalizationRegistry((key) => { // Fires after GC collects the value - not immediate this.#cache.delete(key); console.log(`Cache entry "${key}" was collected`); }); set(key, value) { this.#cache.set(key, new WeakRef(value)); this.#registry.register(value, key); // key is the held value, not the object } get(key) { return this.#cache.get(key)?.deref(); } } const cache = new AutoCache(); let session = { userId: 42, token: "abc123" }; cache.set("session:42", session); console.log(cache.get("session:42")); // { userId: 42, token: "abc123" } session = null; // At some future GC cycle: 'Cache entry "session:42" was collected' ``` The FinalizationRegistry receives `key` as its held value, not the `session` object. Passing `session` to the registry would hold a strong reference to it and prevent collection entirely. ### Advanced: Verifying callback timing ```javascript // FinalizationRegistry callbacks are NOT synchronous let collected = false; const reg = new FinalizationRegistry(() => { collected = true; }); let temp = {}; reg.register(temp, null); temp = null; console.log(collected); // false - callback has not fired yet // In a short-lived script, the callback may never fire at all. // The engine decides when to run GC based on heap pressure, not your code. ``` This is the senior-level trap. Setting a variable to `null` does not trigger GC. The engine runs collection based on internal heuristics. In a script that exits quickly, registered callbacks may never run. For anything that must happen, use explicit cleanup methods.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.