JavaScript 59 🧬 WeakRef / FinalizationRegistry
let obj = { id: 1 };
const ref = new WeakRef(obj);
console.log(ref.deref());
console.log(ref.deref() === obj);
obj = null;
setTimeout(() => {
console.log(ref.deref());
}, 1000);
const registry = new FinalizationRegistry((heldValue) => {
console.log('Collected:', heldValue);
});
let target = { name: 'Alice' };
registry.register(target, 'target-object');
target = null;
setTimeout(() => {
console.log('After GC check');
}, 5000);
class Resource {
constructor(id) {
this.id = id;
}
release() {
console.log('Releasing', this.id);
}
}
const finalizer = new FinalizationRegistry((id) => {
console.log('Resource freed:', id);
});
let resource = new Resource('db-connection');
finalizer.register(resource, resource.id);
resource = null;
JavaScript is garbage-collected — you don’t manage memory manually. But sometimes you need to know when an object is collected, or hold a reference that doesn’t prevent collection. That’s what WeakRef and FinalizationRegistry provide. They’re advanced, rarely needed, and easy to misuse — but essential when the situation calls for them.
Key point: WeakRef lets you hold a reference to an object without keeping it alive. FinalizationRegistry lets you run a callback after an object is garbage-collected. Both are non-deterministic — the GC decides when, and it might never happen. Use them only when you’ve ruled out every other approach.
a – WeakRef
A WeakRef holds a weak reference to an object — a reference that doesn’t prevent garbage collection. When all strong references to the object are gone, the GC can reclaim it, and the WeakRef starts returning undefined.
Basic syntax:
const ref = new WeakRef(obj);
objmust be an object — WeakRef doesn’t work on primitivesref.deref()returns the object, orundefinedif it’s been collected
Getting the object back:
let obj = { id: 1 };
const ref = new WeakRef(obj);
console.log(ref.deref());
// [ { id: 1 } ]
console.log(ref.deref() === obj);
// [ true ]
After the strong reference is gone:
obj = null;
// GC eventually collects the object
// ref.deref() returns undefined
The GC runs when it wants — you can’t force it. So the object may still be alive for a while.
Why WeakRef exists:
Normally, holding a reference to an object keeps it alive — that’s what garbage collection protects against. But some caching and tracking patterns need to hold a hint about an object without preventing its cleanup. WeakRef is for those cases.
WeakRef vs WeakMap and WeakSet:
| Feature | WeakRef | WeakMap | WeakSet |
|---|---|---|---|
| Holds | Single object | Object → value | Object |
| Retrieval | .deref() | .get(key) | .has(obj) |
| Iterable | N/A | ❌ | ❌ |
| Use case | Cache entry | Metadata | Tagging |
A WeakRef is a single weak reference. WeakMap and WeakSet are collections of weak references to keys/values. WeakMap’s keys and WeakSet’s contents don’t prevent collection.
A cached object example:
class DataCache {
constructor() {
this.refs = new Map();
}
get(key) {
const ref = this.refs.get(key);
if (!ref) return null;
const value = ref.deref();
if (!value) {
// GC collected it — remove stale entry
this.refs.delete(key);
return null;
}
return value;
}
set(key, value) {
this.refs.set(key, new WeakRef(value));
}
}
This holds cached objects without preventing them from being collected when memory is tight. The cache entries become stale and clean themselves up lazily.
The deref() method:
const ref = new WeakRef({ a: 1 });
const value = ref.deref();
if (value) {
console.log(value.a);
// [ 1 ]
} else {
console.log('collected');
}
deref() returns either the object or undefined — always check.
WeakRef can’t be used on primitives:
new WeakRef(42);
// TypeError: Invalid value used in weak ref
new WeakRef('string');
// TypeError
new WeakRef({});
// ✅
Only objects — including arrays, functions, and other objects.
The GC is not immediate:
let obj = { id: 1 };
const ref = new WeakRef(obj);
obj = null;
console.log(ref.deref());
// [ { id: 1 } ] ← still alive — GC hasn't run
Even though the strong reference is gone, the object may still be alive — the GC runs when it decides to.
When to use WeakRef:
| Use case | Why |
|---|---|
| Caching without blocking GC | Hold hint, not ownership |
| Tracking “recently used” items | Avoid keeping alive forever |
| Avoiding memory leaks | Cleanup when target dies |
| Optional links between objects | Weak association |
When NOT to use WeakRef:
| Situation | Instead |
|---|---|
| You need the object | Use a strong reference |
| You need deterministic cleanup | Use try/finally |
| You need a proper cache | Use Map with eviction policy |
| You need to observe collection | Use FinalizationRegistry |
WeakRef doesn’t guarantee collection:
The GC may never collect the object if memory pressure never increases. So you can’t rely on WeakRef becoming empty as a signal that something happened.
Common WeakRef patterns:
Pattern 1 — Weak cache with lazy cleanup:
const cache = new Map();
function getCached(key, factory) {
const ref = cache.get(key);
let value = ref?.deref();
if (!value) {
value = factory();
cache.set(key, new WeakRef(value));
}
return value;
}
The cache doesn’t hold objects forever — they can be collected when no one else needs them.
Pattern 2 — Preventing memory leaks in event handlers:
class EventManager {
constructor() {
this.listeners = [];
}
add(target, handler) {
const ref = new WeakRef(target);
const wrapped = (event) => {
const t = ref.deref();
if (t) {
handler.call(t, event);
} else {
// target was collected — remove listener
this.remove(wrapped);
}
};
target.addEventListener('click', wrapped);
this.listeners.push(wrapped);
}
remove(wrapped) {
// ...
}
}
Pattern 3 — Secondary metadata:
class Tracker {
constructor() {
this.items = [];
}
track(obj) {
this.items.push(new WeakRef(obj));
}
active() {
return this.items
.map(ref => ref.deref())
.filter(Boolean);
}
}
Pattern 4 — Symbol-keyed cache:
const weakCache = new WeakMap();
function getMeta(obj) {
let meta = weakCache.get(obj);
if (!meta) {
meta = { created: Date.now() };
weakCache.set(obj, meta);
}
return meta;
}
For metadata, WeakMap is often better than WeakRef — it associates data with an object without extending its lifetime.
When WeakRef is the right tool:
- You want a cache that doesn’t prevent GC
- You want a hint about an object that may or may not still exist
- You want to track objects without owning them
- You’re building infrastructure where holding strong references would leak
WeakRef and memory pressure:
let obj = { data: new Array(1e6).fill(0) };
const ref = new WeakRef(obj);
obj = null;
// Eventually, GC collects the big array
// ref.deref() → undefined
The GC runs when it needs memory. Under memory pressure, it collects more aggressively.
b – FinalizationRegistry
A FinalizationRegistry lets you run a callback when an object is garbage-collected. It’s the other half of the weak-reference toolkit — you use it to react to collection.
Basic syntax:
const registry = new FinalizationRegistry((heldValue) => {
console.log('Object collected:', heldValue);
});
The callback receives a held value — a piece of data you provide at registration time.
Registering:
let obj = { name: 'Alice' };
registry.register(obj, 'my-held-value');
The second argument is passed to the callback when obj is collected.
After collection:
obj = null;
// Some time later, when GC runs:
// [ Object collected: my-held-value ]
Why FinalizationRegistry exists:
Sometimes you have side resources that aren’t tied to the JS heap — open files, network connections, memory outside the JS engine. When the JS object is collected, you want to clean up. FinalizationRegistry is the hook.
A resource cleanup example:
const finalizer = new FinalizationRegistry((id) => {
console.log(`Freed resource ${id}`);
});
class Resource {
constructor(id) {
this.id = id;
finalizer.register(this, id);
}
close() {
console.log(`Closing ${this.id}`);
}
}
{
let r = new Resource('file-1');
// ...
// when `r` is collected, "Freed resource file-1" is logged
}
Warning: Do NOT rely on this for critical cleanup. The GC might never run, or it might run much later than you expect. Always provide an explicit
close()method.
Held values:
The held value is not the object — it’s separate data:
registry.register(target, heldValue);
The held value is kept alive by the registry, even after target is collected. This is how you avoid referencing the target from its own cleanup callback.
const registry = new FinalizationRegistry((heldValue) => {
console.log('Collected:', heldValue);
});
const target = { name: 'Alice' };
registry.register(target, { id: 42 });
// The target is collected; the held value { id: 42 } stays alive
The target is held weakly:
Registration does not prevent the target from being collected.
Unregistering:
registry.unregister(token);
Provide an unregister token at registration:
const token = {};
registry.register(target, 'held', token);
registry.unregister(token);
You can’t unregister by target — you must provide a token.
Complete example:
const registry = new FinalizationRegistry((heldValue) => {
console.log('Cleaning up:', heldValue);
});
const token = {};
let obj = { name: 'Alice' };
registry.register(obj, 'obj-1', token);
// Cancel before collection
registry.unregister(token);
obj = null;
// No cleanup log — we unregistered
Multiple registrations:
You can register the same object multiple times, and even register the same held value with different targets:
registry.register(objA, 'A');
registry.register(objB, 'B');
Each fires independently.
Common FinalizationRegistry patterns:
Pattern 1 — Resource cleanup:
const finalizer = new FinalizationRegistry(({ type, id }) => {
console.log(`Auto-closing ${type} ${id}`);
});
class FileHandle {
constructor(path) {
this.path = path;
this.closed = false;
finalizer.register(this, { type: 'file', id: path });
}
close() {
if (!this.closed) {
this.closed = true;
finalizer.unregister(this); // need token — see next pattern
console.log(`Closed ${this.path}`);
}
}
}
To unregister, you need a token. Store it:
class FileHandle {
constructor(path) {
this.path = path;
this.closed = false;
this.token = {};
finalizer.register(this, { type: 'file', id: path }, this.token);
}
close() {
if (!this.closed) {
this.closed = true;
finalizer.unregister(this.token);
console.log(`Closed ${this.path}`);
}
}
}
Pattern 2 — Tracking object counts:
let alive = 0;
const counter = new FinalizationRegistry(() => {
alive--;
console.log('Remaining:', alive);
});
function create() {
const obj = {};
alive++;
counter.register(obj, null);
return obj;
}
Pattern 3 — Detecting leaks:
const registry = new FinalizationRegistry((id) => {
console.log(`Object ${id} was collected`);
});
const objects = new Map();
let nextId = 0;
function track(obj) {
const id = nextId++;
objects.set(id, new WeakRef(obj));
registry.register(obj, id);
return id;
}
Pattern 4 — Cleanup with side effects:
const registry = new FinalizationRegistry((filePath) => {
fs.unlink(filePath, () => {});
});
function withTempFile(data) {
const path = `/tmp/${Date.now()}`;
fs.writeFileSync(path, data);
const handle = { path };
registry.register(handle, path);
return handle;
}
Pattern 5 — Clearing caches:
const cache = new Map();
const registry = new FinalizationRegistry((key) => {
cache.delete(key);
});
function cacheWith(obj, key) {
cache.set(key, obj);
registry.register(obj, key);
}
When to use FinalizationRegistry:
| Use case | Example |
|---|---|
| Freeing native memory | WASM, WebGL |
| Closing file descriptors | File handles |
| Releasing pooled resources | Connection pools |
| Testing memory | Detecting leaks |
| Side-effect cleanup | Temp file deletion |
When NOT to use FinalizationRegistry:
- Critical cleanup — GC might not run before the program exits
- Deterministic behavior — use explicit
close()methods - Frequent cleanup — the callback overhead adds up
- Web Workers or old browsers — check support
FinalizationRegistry is non-deterministic:
let obj = {};
const registry = new FinalizationRegistry(() => {
console.log('collected');
});
registry.register(obj, null);
obj = null;
console.log('before');
// [ 'before' ]
// ...eventually: [ 'collected' ]
console.log('after');
// [ 'after' ]
// But the "collected" line might never print during this run
If GC doesn’t run before the program exits, the callback never fires.
The registry and its held values keep data alive:
Held values are stored strongly by the registry:
const registry = new FinalizationRegistry((hugeData) => {});
registry.register(obj, new Array(1e6).fill(0));
The big array stays alive until the target is collected. Choose held values carefully.
Browser and Node.js support:
| Environment | Support |
|---|---|
| Node.js | ✅ (v14.6.0+) |
| Chrome | ✅ |
| Firefox | ✅ (v79+) |
| Safari | ✅ (v14.1+) |
| Edge | ✅ |
Combining WeakRef and FinalizationRegistry:
const registry = new FinalizationRegistry((key) => {
cache.delete(key);
console.log(`Cache cleared: ${key}`);
});
const cache = new Map();
function get(key, factory) {
const ref = cache.get(key);
if (ref) {
const value = ref.deref();
if (value) return value;
cache.delete(key); // stale
}
const value = factory();
cache.set(key, new WeakRef(value));
registry.register(value, key);
return value;
}
This is a weak cache with a self-cleaning Map. When an object is collected, the registry removes its entry from the cache.
Warning from the spec:
“The FinalizationRegistry is not a substitute for proper resource management. It may call the callback long after the object is no longer useful, or never.”
Translation: use it as a safety net, not as a primary cleanup mechanism.
c – Practical patterns and pitfalls
Both APIs are easy to misuse. Here are the patterns that work and the mistakes to avoid.
Pattern 1 — Cache with weak values:
function createWeakCache() {
const map = new Map();
return {
get(key) {
const ref = map.get(key);
const value = ref?.deref();
if (!value) {
map.delete(key);
return undefined;
}
return value;
},
set(key, value) {
map.set(key, new WeakRef(value));
}
};
}
The Map doesn’t keep values alive — only WeakRefs do.
Pattern 2 — Weak value store:
class WeakValueStore {
#refs = new Map();
set(key, value) {
this.#refs.set(key, new WeakRef(value));
}
get(key) {
return this.#refs.get(key)?.deref();
}
has(key) {
return this.#refs.get(key)?.deref() !== undefined;
}
cleanup() {
for (const [key, ref] of this.#refs) {
if (!ref.deref()) this.#refs.delete(key);
}
}
}
Periodic cleanup removes stale entries.
Pattern 3 — Cleanup registry with unregister tokens:
class Resource {
#token = {};
constructor() {
finalRegistry.register(this, this, this.#token);
}
destroy() {
finalRegistry.unregister(this.#token);
}
}
Pattern 4 — Tracking alive objects:
const alive = new Map();
let count = 0;
const registry = new FinalizationRegistry((id) => {
alive.delete(id);
count--;
});
function track(obj) {
const id = count++;
alive.set(id, obj);
registry.register(obj, id);
return id;
}
Pattern 5 — Timed cache with WeakRef:
class TimedCache {
#refs = new Map();
set(key, value, ttl) {
this.#refs.set(key, {
ref: new WeakRef(value),
expires: Date.now() + ttl
});
}
get(key) {
const entry = this.#refs.get(key);
if (!entry) return undefined;
if (entry.expires < Date.now()) {
this.#refs.delete(key);
return undefined;
}
const value = entry.ref.deref();
if (!value) this.#refs.delete(key);
return value;
}
}
TTL plus weak reference — object dies when either expires or is collected.
Pattern 6 — Observing collection in tests:
function detectLeaks() {
const collected = [];
const registry = new FinalizationRegistry((id) => {
collected.push(id);
});
return { registry, collected };
}
Useful for library tests to confirm objects aren’t leaked.
Pattern 7 — WASM memory cleanup:
const wasmFinalizer = new FinalizationRegistry((ptr) => {
wasmModule.free(ptr);
});
function withWasmObject() {
const obj = {};
const ptr = wasmModule.alloc();
wasmFinalizer.register(obj, ptr);
return obj;
}
When obj is collected, WASM memory is freed.
Pattern 8 — WebGL resource cleanup:
const glFinalizer = new FinalizationRegistry((buffer) => {
gl.deleteBuffer(buffer);
});
function createBuffer() {
const holder = {};
const buffer = gl.createBuffer();
glFinalizer.register(holder, buffer);
return holder;
}
Pitfall 1 — WeakRef doesn’t force GC:
let obj = { data: new Array(1e6).fill(0) };
const ref = new WeakRef(obj);
obj = null;
// This may still be alive
console.log(ref.deref());
// [ { data: [...] } ]
The GC decides when to run. deref() might return the object for a while.
Pitfall 2 — Can’t unregister by target:
registry.register(target, held);
registry.unregister(target);
// ❌ — unregister needs a token, not the target
You must provide the token at registration time and use that.
Pitfall 3 — Held value keeps memory alive:
registry.register(obj, bigArray);
bigArray stays alive until obj is collected. Use minimal held values.
Pitfall 4 — Callbacks run at unpredictable times:
registry.register(obj, () => {
closeConnection();
});
You can’t predict when closeConnection() runs — it might be seconds later, minutes later, or never.
Pitfall 5 — Don’t use for critical cleanup:
// ❌ Don't rely on this to flush a file
registry.register(file, () => {
file.close();
});
// ✅ Do provide explicit close()
file.close();
Always provide a manual cleanup method. The finalizer is a safety net.
Pitfall 6 — Memory leak in the registry itself:
for (let i = 0; i < 1e6; i++) {
registry.register({}, i);
}
Registrations pile up until their targets are collected. If targets are short-lived, the registry grows until the GC runs.
Pitfall 7 — Async cleanup:
registry.register(obj, async () => {
await cleanup();
});
Finalizers should be synchronous. Async cleanup can leak or be skipped.
Pitfall 8 — Using FinalizationRegistry for control flow:
// ❌ Don't do this
let done = false;
registry.register(obj, () => { done = true; });
while (!done) { /* wait */ }
// Hangs forever if GC never runs
Never wait on a finalizer.
Pitfall 9 — Multiple registries for the same object:
registryA.register(obj, 'A');
registryB.register(obj, 'B');
Both fire, in no guaranteed order.
Pitfall 10 — WeakRef on primitives:
new WeakRef(42);
// TypeError
Only objects.
Pitfall 11 — Reusing the same held value:
registry.register(objA, sharedState);
registry.register(objB, sharedState);
The callback runs twice with the same held value. Make sure your callback handles that.
Pitfall 12 — GC timing differs across engines:
// Browser A might run GC at time X
// Browser B might run it at time Y
// Node might run it at time Z
Never write code that depends on GC timing.
Pitfall 13 — FinalizationRegistry callbacks and microtasks:
registry.register(obj, () => {
queueMicrotask(() => console.log('after'));
});
The callback runs during a GC microtask — you can schedule more work, but keep it simple.
Pitfall 14 — WeakRef and function closures:
function makeCounter() {
let count = 0;
const ref = new WeakRef(() => count++);
return ref;
}
The closure is referenced by the WeakRef. If nothing else holds it, it’s collectible.
Pitfall 15 — Circular references and GC:
Modern GC handles cycles:
const a = {};
const b = {};
a.b = b;
b.a = a;
a and b will still be collected when no external references remain.
Good practice — always provide explicit cleanup:
class Connection {
constructor() {
this.closed = false;
registry.register(this, this, this);
}
close() {
if (this.closed) return;
this.closed = true;
registry.unregister(this);
// real close logic
}
}
The finalizer is a fallback if the caller forgets. The explicit close() is the primary path.
When to reach for WeakRef:
- You’re building a cache that shouldn’t prevent GC
- You’re tracking objects weakly
- You want to avoid strong references in long-lived structures
When to reach for FinalizationRegistry:
- You need to release native resources (WASM, WebGL, files)
- You want to detect when cached objects are collected
- You’re writing a library that must clean up external state
- You want to observe leaks in tests
When neither is the right tool:
- Deterministic cleanup — use
try/finallyor explicitclose() - Ownership tracking — use a strong reference
- General caching — use
Mapwith an eviction policy (LRU) - Reference counting — not applicable in JS
The most important rule:
WeakRefandFinalizationRegistryare advanced tools for infrastructure. If you’re not writing library internals, you probably don’t need them. And even then, always provide explicit cleanup.
Complete Example Session
// ============================================
// PART 1: BASIC WEAKREF
// ============================================
let obj = { id: 1 };
const ref = new WeakRef(obj);
console.log(ref.deref());
// [ { id: 1 } ]
console.log(ref.deref() === obj);
// [ true ]
// ============================================
// PART 2: AFTER STRONG REF DROPS
// ============================================
obj = null;
console.log(ref.deref());
// [ { id: 1 } ] ← still alive — GC hasn't run
setTimeout(() => {
console.log(ref.deref());
// [ undefined ] or [ { id: 1 } ] depending on GC
}, 1000);
// ============================================
// PART 3: WEAKREF REQUIRES OBJECT
// ============================================
try {
new WeakRef(42);
} catch (err) {
console.log(err.message);
}
// [ Invalid value used in weak ref ]
// ============================================
// PART 4: WEAK CACHE
// ============================================
class Cache {
#refs = new Map();
get(key) {
const ref = this.#refs.get(key);
const value = ref?.deref();
if (!value) this.#refs.delete(key);
return value;
}
set(key, value) {
this.#refs.set(key, new WeakRef(value));
}
}
const cache = new Cache();
let cached = { data: 'x' };
cache.set('a', cached);
console.log(cache.get('a')?.data);
// [ 'x' ]
// ============================================
// PART 5: FINALIZATIONREGISTRY
// ============================================
const registry = new FinalizationRegistry((heldValue) => {
console.log('Collected:', heldValue);
});
let target = { name: 'Alice' };
registry.register(target, 'target-object');
target = null;
setTimeout(() => {
console.log('done');
}, 5000);
// ============================================
// PART 6: HELD VALUE
// ============================================
const heldValue = { id: 42 };
const registry2 = new FinalizationRegistry((value) => {
console.log('Held value:', value.id);
});
let t2 = {};
registry2.register(t2, heldValue);
t2 = null;
// ============================================
// PART 7: UNREGISTER WITH TOKEN
// ============================================
const registry3 = new FinalizationRegistry((v) => {
console.log('Should not fire:', v);
});
const token = {};
let t3 = {};
registry3.register(t3, 'x', token);
registry3.unregister(token);
t3 = null;
// No callback fires
// ============================================
// PART 8: RESOURCE CLEANUP
// ============================================
class Resource {
constructor(id) {
this.id = id;
this.token = {};
registry.register(this, id, this.token);
}
release() {
console.log('Explicit release:', this.id);
registry.unregister(this.token);
}
}
{
let r = new Resource('res-1');
r.release();
// [ 'Explicit release: res-1' ]
}
// ============================================
// PART 9: WEAKREF + FINALIZATION
// ============================================
const cleanupCache = new Map();
const registry4 = new FinalizationRegistry((key) => {
cleanupCache.delete(key);
console.log('Auto-cleaned:', key);
});
function cacheSet(key, value) {
cleanupCache.set(key, new WeakRef(value));
registry4.register(value, key);
}
let v = { data: 1 };
cacheSet('k', v);
console.log(cleanupCache.has('k'));
// [ true ]
// ============================================
// PART 10: NOT PREDICTABLE
// ============================================
let obj4 = {};
const ref4 = new WeakRef(obj4);
obj4 = null;
console.log(ref4.deref() !== undefined);
// [ true ] — object still alive
// GC timing is not guaranteed
// ============================================
// PART 11: NO PRIMITIVES
// ============================================
try {
new WeakRef('hello');
} catch (err) {
console.log(err.message);
}
// [ Invalid value used in weak ref ]
// ============================================
// PART 12: SAME OBJECT MULTIPLE REGISTRATIONS
// ============================================
const registry5 = new FinalizationRegistry((v) => console.log(v));
let obj5 = {};
registry5.register(obj5, 'A');
registry5.register(obj5, 'B');
obj5 = null;
// Eventually both callbacks fire
// ============================================
// PART 13: HELD VALUE KEEPS MEMORY ALIVE
// ============================================
const registry6 = new FinalizationRegistry(() => {});
let big = { data: new Array(1e6).fill(0) };
registry6.register({}, big); // ❌ big kept alive until target dies
// ============================================
// PART 14: SAFETY NET PATTERN
// ============================================
class File {
#token = {};
#closed = false;
constructor(path) {
this.path = path;
finalRegistry.register(this, this.path, this.#token);
}
close() {
if (this.#closed) return;
this.#closed = true;
finalRegistry.unregister(this.#token);
console.log('Explicit close:', this.path);
}
}
// ============================================
// PART 15: TRACKING COUNTS
// ============================================
let alive = 0;
const countRegistry = new FinalizationRegistry(() => {
alive--;
});
function track() {
const obj = {};
alive++;
countRegistry.register(obj, null);
return obj;
}
// ============================================
// PART 16: WEAKREF IN MAP
// ============================================
const store = new Map();
function weakSet(key, value) {
store.set(key, new WeakRef(value));
}
function weakGet(key) {
return store.get(key)?.deref();
}
let val = { x: 1 };
weakSet('a', val);
console.log(weakGet('a').x);
// [ 1 ]
// ============================================
// PART 17: WEAKREF DOES NOT PREVENT COLLECTION
// ============================================
function test() {
let local = { name: 'temp' };
const ref = new WeakRef(local);
return ref;
}
const returned = test();
// local is out of scope; only the WeakRef remains
// GC may collect it at any time
// ============================================
// PART 18: COMBINED CACHE
// ============================================
class WeakCache {
#map = new Map();
#finalizer;
constructor() {
this.#finalizer = new FinalizationRegistry((key) => {
this.#map.delete(key);
});
}
set(key, value) {
this.#map.set(key, new WeakRef(value));
this.#finalizer.register(value, key);
}
get(key) {
return this.#map.get(key)?.deref();
}
}
// ============================================
// PART 19: UNREGISTER SAVES MEMORY
// ============================================
const r = new FinalizationRegistry(() => {});
let saved = {};
const t = {};
r.register(saved, 'huge-held-value', t);
r.unregister(t);
// held value is freed when unregistered
// ============================================
// PART 20: FULL SCRIPT
// ============================================
let obj59 = { id: 1 };
const ref59 = new WeakRef(obj59);
console.log(ref59.deref());
console.log(ref59.deref() === obj59);
obj59 = null;
setTimeout(() => {
console.log(ref59.deref());
}, 1000);
const registry59 = new FinalizationRegistry((heldValue) => {
console.log('Collected:', heldValue);
});
let target59 = { name: 'Alice' };
registry59.register(target59, 'target-object');
target59 = null;
setTimeout(() => {
console.log('After GC check');
}, 5000);
class Resource59 {
constructor(id) {
this.id = id;
}
release() {
console.log('Releasing', this.id);
}
}
const finalizer59 = new FinalizationRegistry((id) => {
console.log('Resource freed:', id);
});
let resource59 = new Resource59('db-connection');
finalizer59.register(resource59, resource59.id);
resource59 = null;
Quick Reference
WeakRef
| Method | Purpose |
|---|---|
new WeakRef(obj) | Create weak reference |
ref.deref() | Get object or undefined |
FinalizationRegistry
| Method | Purpose |
|---|---|
new FinalizationRegistry(cb) | Create registry |
registry.register(target, held, token?) | Register |
registry.unregister(token) | Cancel |
WeakRef vs Other
| Feature | WeakRef | WeakMap | WeakSet | Map |
|---|---|---|---|---|
| Holds | Object | Object → value | Object | Any → any |
| Retrieval | .deref() | .get(k) | .has(v) | .get(k) |
| Iterable | ❌ | ❌ | ❌ | ✅ |
| Size | ❌ | ❌ | ❌ | ✅ |
| Clears | On GC | On GC | On GC | Manual |
WeakMap vs WeakRef
| Use case | Better choice |
|---|---|
| Metadata for object | WeakMap |
| Cache values by object | WeakMap |
| Weak pointer to object | WeakRef |
| Track alive objects | WeakRef |
FinalizationRegistry vs Alternatives
| Task | Tool |
|---|---|
| Explicit cleanup | try/finally |
| Native resource | FinalizationRegistry |
| Cache eviction | LRU Map |
| Leak detection | FinalizationRegistry |
When to Use
| Use WeakRef when | Use FinalizationRegistry when |
|---|---|
| You need a weak cache | You release native memory |
| You track objects weakly | You clean up side effects |
| You avoid leaks | You observe collection |
When NOT to Use
| Don’t use WeakRef when | Don’t use FinalizationRegistry when |
|---|---|
| You need the object | Cleanup must be deterministic |
| You need deterministic behavior | Task must run before exit |
| A normal Map works | A try/finally works |
Browser / Node Support
| Environment | WeakRef | FinalizationRegistry |
|---|---|---|
| Node.js 14.6+ | ✅ | ✅ |
| Chrome 84+ | ✅ | ✅ |
| Firefox 79+ | ✅ | ✅ |
| Safari 14.1+ | ✅ | ✅ |
Best Practices
✅ Do This:
// Always check deref result
const value = ref.deref();
if (value) { /* use value */ } // ✅
// Provide explicit cleanup
class Res {
close() {
this.#closed = true;
registry.unregister(this.#token);
}
} // ✅
// Use tokens to unregister
const token = {};
registry.register(obj, held, token);
registry.unregister(token); // ✅
// Keep held values minimal
registry.register(obj, id); // ✅
// Use WeakMap for metadata
const meta = new WeakMap(); // ✅
// Use WeakRef for weak caches
map.set(key, new WeakRef(value)); // ✅
// Detect leaks in tests
const registry = new FinalizationRegistry(...) // ✅
❌ Don’t Do This:
// Don't rely on GC timing
await waitForGC(); // ❌ impossible
// Don't use WeakRef for critical data
const ref = new WeakRef(config); // ⚠️ config may vanish
// Don't use primitives
new WeakRef(42); // ❌ TypeError
// Don't forget held values hold memory
registry.register(obj, bigArray); // ⚠️ bigArray stays
// Don't unregister without token
registry.unregister(obj); // ❌ wrong API
// Don't do async cleanup in finalizer
registry.register(obj, async () => {}); // ⚠️ may not finish
// Don't use for control flow
while (!collected) {} // ❌ hangs
// Don't expect immediate collection
obj = null;
ref.deref(); // ⚠️ may return object
// Don't use FinalizationRegistry for critical cleanup
// Always provide an explicit close()
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Timing assumption | GC is non-deterministic | Never rely on timing |
| Primitive WeakRef | TypeError | Objects only |
| Held values hold memory | Leaks | Keep them small |
| Unregister without token | Wrong API | Store the token |
| Async finalizer | May not run | Keep synchronous |
| Critical cleanup | GC may skip | Explicit close |
| Waiting on GC | Hangs | Never block |
| Many registrations | Memory growth | Unregister when done |
| Proxy of WeakRef | Weird behavior | Don’t proxy them |
deref() unchecked | undefined errors | Always check |
Real-World Examples
1. Basic WeakRef
let obj = { id: 1 };
const ref = new WeakRef(obj);
console.log(ref.deref().id);
// [ 1 ]
2. Deref After Null
let obj = { id: 1 };
const ref = new WeakRef(obj);
obj = null;
console.log(ref.deref() !== undefined);
// [ true ] or [ false ] — depends on GC
3. WeakRef Requires Object
try {
new WeakRef(42);
} catch (err) {
console.log(err.message);
}
// [ Invalid value used in weak ref ]
4. Weak Cache
const cache = new Map();
function set(key, value) {
cache.set(key, new WeakRef(value));
}
5. Deref and Delete Stale
function get(key) {
const ref = cache.get(key);
const value = ref?.deref();
if (!value) cache.delete(key);
return value;
}
6. Basic Registry
const registry = new FinalizationRegistry((held) => {
console.log('gone:', held);
});
let obj = {};
registry.register(obj, 'id-1');
obj = null;
7. Unregister
const token = {};
registry.register(obj, 'held', token);
registry.unregister(token);
8. Resource Cleanup
class File {
constructor(path) {
this.path = path;
this.token = {};
registry.register(this, path, this.token);
}
close() {
registry.unregister(this.token);
}
}
9. Count Objects
let alive = 0;
const registry = new FinalizationRegistry(() => alive--);
function create() {
const obj = {};
alive++;
registry.register(obj, null);
return obj;
}
10. Combined Weak Cache
class WeakCache {
#map = new Map();
#finalizer = new FinalizationRegistry((k) => this.#map.delete(k));
set(k, v) {
this.#map.set(k, new WeakRef(v));
this.#finalizer.register(v, k);
}
get(k) {
return this.#map.get(k)?.deref();
}
}
11. Tracking Alive Objects
const alive = new Map();
let nextId = 0;
const registry = new FinalizationRegistry((id) => {
alive.delete(id);
});
function track(obj) {
const id = nextId++;
alive.set(id, obj);
registry.register(obj, id);
}
12. Leak Detection
const registry = new FinalizationRegistry((id) => {
console.log('Leaked:', id);
});
for (let i = 0; i < 100; i++) {
registry.register({}, i);
}
13. WASM Cleanup
const wasmFinalizer = new FinalizationRegistry((ptr) => {
wasm.free(ptr);
});
function makeWasmObj() {
const holder = {};
wasmFinalizer.register(holder, wasm.alloc());
return holder;
}
14. WebGL Buffer
const glFinalizer = new FinalizationRegistry((buffer) => {
gl.deleteBuffer(buffer);
});
15. Held Value Keeps Memory
const big = new Array(1e6).fill(0);
registry.register({}, big); // big stays alive
16. Multiple Registrations
registry.register(obj, 'A');
registry.register(obj, 'B');
// Both fire when obj is collected
17. Explicit Close Preferred
class Conn {
close() {
// primary cleanup
}
constructor() {
registry.register(this, this); // fallback only
}
}
18. WeakRef in Map
const map = new Map();
map.set('k', new WeakRef({ data: 1 }));
const value = map.get('k')?.deref();
19. Deref Check
const value = ref.deref();
if (value) {
console.log(value);
} else {
console.log('collected');
}
20. Full Script
let obj59 = { id: 1 };
const ref59 = new WeakRef(obj59);
console.log(ref59.deref());
console.log(ref59.deref() === obj59);
obj59 = null;
setTimeout(() => {
console.log(ref59.deref());
}, 1000);
const registry59 = new FinalizationRegistry((heldValue) => {
console.log('Collected:', heldValue);
});
let target59 = { name: 'Alice' };
registry59.register(target59, 'target-object');
target59 = null;
setTimeout(() => {
console.log('After GC check');
}, 5000);
class Resource59 {
constructor(id) {
this.id = id;
}
release() {
console.log('Releasing', this.id);
}
}
const finalizer59 = new FinalizationRegistry((id) => {
console.log('Resource freed:', id);
});
let resource59 = new Resource59('db-connection');
finalizer59.register(resource59, resource59.id);
resource59 = null;
Visual: WeakRef Lifecycle
┌──────────────────────────────────────────────┐
│ let obj = { data: 1 }; │
│ const ref = new WeakRef(obj); │
│ │
│ obj ──► { data: 1 } ← strong │
│ ref ──► { data: 1 } ← weak (deref) │
│ │
│ Object alive, deref returns it │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ obj = null; │
│ │
│ ref ──► { data: 1 } ← weak only │
│ │
│ GC may collect at any time │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ After GC: │
│ │
│ ref ──► (nothing) │
│ │
│ ref.deref() → undefined │
│ │
└──────────────────────────────────────────────┘
Visual: FinalizationRegistry Flow
┌──────────────────────────────────────────────┐
│ const registry = new FinalizationRegistry( │
│ (held) => console.log(held) │
│ ); │
│ │
│ let obj = {}; │
│ registry.register(obj, 'my-id'); │
│ │
│ Object → registry │
│ └── held value: 'my-id' │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ obj = null; │
│ │
│ GC runs → object collected │
│ │
│ registry callback fires: │
│ console.log('my-id') │
│ │
│ Timing: whenever GC decides │
│ │
└──────────────────────────────────────────────┘
Visual: WeakRef vs Strong Reference
┌──────────────────────────────────────────────┐
│ Strong reference │
│ │
│ variable ──► object │
│ │
│ Object stays alive as long as variable │
│ points to it │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Weak reference │
│ │
│ WeakRef ──► object │
│ (doesn't count) │
│ │
│ Object can be collected even if WeakRef │
│ points to it │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Syntax | Purpose |
|---|---|---|
| WeakRef | new WeakRef(obj) | Weak reference |
| Deref | ref.deref() | Get or undefined |
| Registry | new FinalizationRegistry(cb) | Collection callback |
| Register | registry.register(t, held, token?) | Register |
| Unregister | registry.unregister(token) | Cancel |
| Held value | Second arg to register | Data for callback |
| Token | Third arg to register | Unregister key |
Key takeaways:
WeakRefholds a weak reference — doesn’t prevent GC.deref()returns the object orundefined- Works only on objects — not primitives
FinalizationRegistryruns a callback after an object is collected- Registration is weak — doesn’t keep the target alive
- Held values stay alive until the target is collected
- Use tokens to unregister before collection
- GC timing is not deterministic — never rely on when callbacks fire
- Never use for critical cleanup — always provide explicit
close() - Use WeakRef for weak caches that shouldn’t prevent collection
- Use FinalizationRegistry for native resource cleanup (WASM, WebGL, files)
- WeakMap is often better than WeakRef for metadata association
- Don’t over-engineer — if a normal
Mapor explicit cleanup works, use it
Remember: WeakRef and FinalizationRegistry are advanced tools for infrastructure code — library internals, memory-sensitive caches, and native resource management. They exist so you can hold a hint about an object or clean up when it disappears, without preventing collection. But they’re non-deterministic, easy to misuse, and rarely the right answer for application code. Use them sparingly, always provide explicit cleanup, and never depend on the GC to do something at a specific time.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!