new FinalizationRegistry(callback) asks for callback(heldValue) to run some time after an object registered with register(target, heldValue, unregisterToken?) is collected; unregister(token) cancels it. The held value must not reference the target, or it is never collected. Typically it cleans up after WeakRef, such as stale cache keys:
// node --expose-gc cache.mjs
const cache = new Map(); // url -> WeakRef(thumbnail)
const registry = new FinalizationRegistry((url) => {
if (!cache.get(url)?.deref()) cache.delete(url); // the entry may have been refilled
console.log('collected', url, 'entries:', cache.size);
});
function thumbnail(url) {
const cached = cache.get(url)?.deref();
if (cached) return cached;
const thumb = { url, bytes: new Uint8Array(1_000_000) };
cache.set(url, new WeakRef(thumb));
registry.register(thumb, url); // held value must not be thumb
return thumb;
}
let a = thumbnail('a.png');
const b = thumbnail('b.png');
a = null;
setTimeout(() => { globalThis.gc(); console.log(b.url, 'still in use'); });b.png still in use collected a.png entries: 1
Callbacks never run synchronously, may run late or never, and do not run when the page or process exits. Release files, sockets and locks with explicit close() or dispose() methods, not finalizers.