Question :
How do WeakRef and FinalizationRegistry differ from normal JavaScript references, and when might they be useful in a Node.js application?
Detailed Solution:
A normal reference is a strong reference.
let user = {
name: "Rahul",
};
let anotherReference = user;
user = null;
The object is still reachable through anotherReference.
Conceptually:
user ───────► Object
▲
│
anotherReference
Because at least one strong reference exists, the garbage collector cannot remove the object.
If both references disappear:
user = null;
anotherReference = null;
then the object may eventually become eligible for garbage collection.
WeakRef?WeakRef provides a weak reference to an object.
Unlike a normal reference, a WeakRef does not keep the object alive.
Example:
let user = {
name: "Rahul",
};
const weakUser = new WeakRef(user);
console.log(weakUser.deref());
deref() gives you the object if it is still alive.
You might get:
{ name: "Rahul" }
However, if the object becomes unreachable elsewhere:
user = null;
the garbage collector is free to remove it.
After garbage collection:
console.log(weakUser.deref());
may return:
undefined
The important point is that you cannot control when this happens.
Consider this:
letuser= {
name:"Rahul"
};constweakUser=newWeakRef(user);
The relationship looks conceptually like:
user ─────────► Object
▲
│
WeakRef
The normal variable user is a strong reference.
The WeakRef is not considered a strong reason for keeping the object alive.
So when:
user = null;
the object can become unreachable:
user ─────► null
WeakRef ───► Object
The WeakRef alone does not prevent garbage collection.
FinalizationRegistry?FinalizationRegistry is used when you want to be notified that an object has been garbage-collected.
Example:
constregistry=newFinalizationRegistry((value) => {console.log(`Object was collected:${value}`);
});letuser= {
name:"Rahul"
};registry.register(user,"user-object");user = null;
Eventually, after the garbage collector determines that the object is no longer reachable, the registry callback may execute:
Object was collected: user-object
The important word here is "may."
You should not assume that the callback will execute immediately.
Garbage collection is controlled by the JavaScript engine.
When you write:
user = null;
it does not mean:
delete object immediately
→ execute FinalizationRegistry callback
Instead:
user = null
↓
Object becomes potentially unreachable
↓
Garbage collector decides when to run
↓
Object is collected
↓
FinalizationRegistry callback may be scheduled
The timing is nondeterministic.
Therefore, this would be bad:
registry.register(databaseConnection, () => {
databaseConnection.close();
});
if your application requires the connection to be closed at a specific time.
For important resources, you should explicitly release them.
WeakRef vs Normal ReferenceThe key difference is whether the reference keeps the object alive.
| Feature | Normal Reference | WeakRef |
|---|---|---|
| Keeps object alive? | Yes | No |
| Object can be garbage-collected? | Not while a strong reference exists | Yes |
| Can access object? | Directly | deref() |
Can return undefined? | Normally no | Yes |
| Garbage collection timing predictable? | No | No |
| Typical use | Normal application objects | Caches and optional references |
For example:
const cache = new Map();
let data = {
value: "large data",
};
cache.set("data", data);
data = null;
The object is still alive because:
cache → data
let data = {
value: "large data",
};
const weakData = new WeakRef(data);
data = null;
Now the WeakRef does not prevent garbage collection.
One useful application is an optional cache.
Imagine a Node.js application creates expensive objects:
class UserProfile {
constructor(id) {
this.id = id;
this.largeData = new Array(100000);
}
}
You might want to cache these objects:
const cache = new Map();
const profile = new UserProfile(101);
cache.set(101, profile);
But a normal Map strongly references the object.
That means the cache itself can prevent garbage collection.
A WeakMap can sometimes be more appropriate when the cache is keyed by an object:
constcache=newWeakMap();letuser= {
id:101
};cache.set(user,newUserProfile(101));user = null;
Once the key object becomes unreachable elsewhere, the corresponding WeakMap entry can become collectible as well.
This is useful for metadata associated with objects, where the metadata should not extend the lifetime of the key.
WeakRef for Optional CachingWeakRef can also be useful when you want a cache that says:
"Use this object if it is still available, but don't keep it alive just because it's cached."
Example:
const cache = new Map();
function addToCache(id, object) {
cache.set(id, new WeakRef(object));
}
function getFromCache(id) {
const ref = cache.get(id);
if (!ref) {
return undefined;
}
const object = ref.deref();
if (!object) {
cache.delete(id);
}
return object;
}
Now the cache stores:
Map
│
├── "user1" → WeakRef ──► Object
└── "user2" → WeakRef ──► Object
The WeakRef doesn't force those objects to remain in memory.
If an object is collected:
ref.deref()
can return:
undefined
and the cache can remove the stale entry.
FinalizationRegistry Fit?Suppose we have objects that correspond to some auxiliary information.
We can register them:
constregistry=newFinalizationRegistry((id) => {console.log(`Object${id} was garbage-collected`);
});functioncreateUser(id) {constuser= {
id
};registry.register(user,id);returnuser;
}letuser=createUser(101);user = null;
At some later point, the callback may run:
Object 101 was garbage-collected
This can be useful for:
This is probably the most important interview point.
You should not use FinalizationRegistry as a replacement for explicit resource management.
For example:
const registry = new FinalizationRegistry((resource) => {
resource.close();
});
It might look convenient, but it's unsafe as the primary mechanism for important resources.
Why?
Because you don't know exactly when the garbage collector will run.
It could be:
milliseconds later
or:
much later
or the callback might not run before the process exits.
Therefore, for something like:
you should generally release them explicitly.
For example:
const connection = createConnection();
try {
// Use connection
} finally {
connection.close();
}
FinalizationRegistry can potentially serve as a backup mechanism, but it should not be treated as deterministic cleanup.
WeakRefThere is another important concept.
Consider:
const weak = new WeakRef(object);
const obj = weak.deref();
if (obj) {
// Use obj
}
Once deref() returns the object, you have a strong reference to it through obj for as long as obj remains reachable.
So:
const obj = weak.deref();
temporarily brings the object back into the normal strong-reference world.
That's why code using WeakRef should be designed carefully.
In real Node.js applications, these features can be useful in situations such as:
You want cached objects to disappear naturally when memory pressure causes them to become unreachable.
You want to associate information with an object without keeping that object alive.
For this, WeakMap is often more appropriate than WeakRef.
FinalizationRegistry can help observe whether objects are eventually being garbage-collected.
For objects that are expensive to recreate but shouldn't be kept alive solely because of a secondary reference, WeakRef can sometimes be useful.
In advanced Node.js applications, finalization mechanisms can sometimes be used as a safety net around resources managed outside normal JavaScript memory management.
But deterministic cleanup is still preferred.
WeakRef vs FinalizationRegistryThey solve related but different problems.
WeakRefAnswers:
"Can I access this object if it is still alive without keeping it alive?"
Example:
const ref = new WeakRef(object);
const objectIfAlive = ref.deref();
FinalizationRegistryAnswers:
"Can I be notified after this object has become unreachable and been garbage-collected?"
Example:
const registry = new FinalizationRegistry((value) => {
console.log(value);
});
registry.register(object, "object collected");