Lesson 45 of 80
A normal reference keeps an object alive. A WeakRef lets you point at one without stopping the garbage collector from taking it back.
14 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Every ordinary variable, object property, or array slot that holds a reference to an object is a strong reference — as long as it exists, the garbage collector will not reclaim that object, no matter how big or unused it is. WeakRef gives you a reference that doesn't count: you can ask a WeakRef for the object it points to, but holding the WeakRef itself never stops that object from being collected.
FinalizationRegistry is the companion piece: it lets you register a callback that might run after an object has actually been garbage collected, so you can clean up something related — closing a file handle, decrementing an external counter — that was tied to that object's lifetime.
Both exist for the same narrow purpose: observing garbage collection from the outside, without ever interfering with it.
A normal Map-based cache keeps every entry alive forever, because a Map's values are strong references — even a cache nobody's reading from anymore holds onto every object it ever cached.
class WeakCache {
#refs = new Map(); // key -> WeakRef(value)
#registry = new FinalizationRegistry((key) => {
this.#refs.delete(key); // clean up the dead entry once GC confirms it
});
set(key, value) {
this.#refs.set(key, new WeakRef(value));
this.#registry.register(value, key);
}
get(key) {
const ref = this.#refs.get(key);
return ref?.deref(); // undefined if GC already reclaimed it
}
}
const cache = new WeakCache();
let bigObject = { data: new Array(1_000_000).fill("x") };
cache.set("key1", bigObject);
cache.get("key1"); // the object — still alive, we're holding `bigObject`
bigObject = null; // drop the only strong reference
// sometime later, once GC actually runs:
cache.get("key1"); // undefined — collected, and the registry callback tidied up #refs
The cache holds each value only through a WeakRef, so caching something never keeps it alive on its own — it's just an opportunistic hint. If nothing else in the program needs bigObject anymore, GC can reclaim it, deref() starts returning undefined, and the registry callback eventually removes the stale Map entry too.
FinalizationRegistry callback might run seconds later, might run right before the page unloads, or in principle might never run at all. Never use it for anything that must happen deterministically, like releasing a lock or closing a network connection on a strict timeline.FinalizationRegistry callback receiving its held value doesn't make that value alive again. The second argument to .register() is a separate plain value (here, the key string) chosen specifically because it must never be a strong reference back to the object being collected — passing the object itself back in would defeat the entire point.WeakMap and WeakSet already let a key be garbage-collected without you doing anything — but you can never enumerate them or ask "is this still around," precisely so nothing can infer collection timing from watching them. WeakRef and FinalizationRegistry are the deliberate exception: they exist specifically to let you observe collection happening, in the narrow cases where you actually need to react to it.