FinalizationRegistry

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:

A cache of weakly held thumbnails that removes dead entriesJavaScriptLive
// 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'); });

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.