FinalizationRegistry
FinalizationRegistry and WeakRef — non-deterministic cleanup hooks, what never to put in a finalizer, and rare valid use cases.
- javascript
- finalizationregistry
- weakref
- gc
FinalizationRegistry lets you register a callback that may run after an object becomes garbage. It is not a destructor. It is not reliable for freeing critical resources. Interviews bring it up to see whether you understand GC non-determinism — and whether you avoid building business logic on finalizers.
API sketch
const registry = new FinalizationRegistry((heldValue) => {
// object was GC'd; heldValue is whatever you registered (NOT the object)
console.log('cleaned', heldValue);
});
function track(obj, id) {
registry.register(obj, id /*, unregisterToken */);
}
{
const cacheEntry = { data: new ArrayBuffer(1024) };
track(cacheEntry, 'entry-1');
} // when cacheEntry becomes unreachable, callback may run later
The held value must not keep the object alive. Usually a string id or a small descriptor.
const token = {};
registry.register(obj, 'res-1', token);
// later if you free manually:
registry.unregister(token);
Pair with WeakRef
const ref = new WeakRef(element);
// later
const el = ref.deref();
if (el) {
// still alive
} else {
// collected
}
WeakRef reads can resurrect timing subtleties; engines have restrictions about when deref is allowed in the same turn as creation. Read the spec notes before shipping clever caches.
What finalizers are for
Rare, valid-ish:
- Diagnostic counters (“how many widgets leaked?”)
- Secondary caches that can drop offline resources
- WASM/native handle tables only as a last-resort safety net after explicit free
Never for:
- Closing sockets that must close
- Unlocking mutexes / releasing file locks
- User-visible side effects
- Anything that must run for correctness
// BAD idea
const registry = new FinalizationRegistry((socket) => {
socket.close(); // may never run; order undefined
});
Always expose an explicit dispose() / Symbol.dispose and use await using / try/finally when the environment supports it.
Why non-deterministic
GC runs when the engine decides. An object can remain reachable longer than you think (closures, detached DOM, maps). Finalizers may run much later, in batch, or — theoretically in some embeddings — not at all before process exit.
// forcing GC is non-standard (e.g. --expose-gc in Node tests)
// never rely on it in app code
Interview answer
“FinalizationRegistry schedules a callback after an object is garbage-collected, receiving a held value you provided—not the object. It’s non-deterministic and unsuitable for required resource cleanup. Prefer explicit dispose. WeakRef gives a weak pointer via deref(); both are advanced tools for caches and diagnostics, not everyday app logic.”
Related
Interaction with WeakMap caches
const cache = new WeakMap();
function getMaterialized(source) {
let view = cache.get(source);
if (!view) {
view = expensiveView(source);
cache.set(source, view);
}
return view;
}
WeakMap often removes the need for FinalizationRegistry: when source is collected, the cache entry disappears without a callback. Prefer that model. Use FinalizationRegistry only when you must actively release an external resource id that the GC cannot see (e.g., a handle table in WASM) — and still expose explicit free() as the primary path.
Scheduling and observability
Finalizer callbacks run on the engine’s schedule, often during GC, with limited ability to allocate or take locks (host-defined). Keep them tiny: push an id onto a queue and process later on a macrotask.
const pending = [];
const registry = new FinalizationRegistry((id) => {
pending.push(id);
queueMicrotask(flush); // or setTimeout(flush, 0)
});
Never depend on ordering between finalizers. Treat them as best-effort telemetry.
Further reading
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.