Commit 9a9f7e87b8f for nodejs
commit 9a9f7e87b8f63eac51604d04ce014531a693d7c0
Author: Matteo Collina <hello@matteocollina.com>
Date: Sun Oct 18 08:36:43 2026 +0200
process: return unregister fn from finalization
process.finalization.register() and registerBeforeExit() now return
a function that removes only that registration, leaving other
registrations for the same object in place. The function does not
hold a strong reference to the object.
Each registration now uses its WeakRef as the unregister token, which
also makes process.finalization.unregister() remove the entries from
the FinalizationRegistry; previously it passed the object as the
token, which was never registered, so the call had no effect.
Refs: https://github.com/nodejs/node/pull/66482
Signed-off-by: Matteo Collina <matteo.collina@gmail.com>
Assisted-by: pi
PR-URL: https://github.com/nodejs/node/pull/66482
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
diff --git a/doc/api/process.md b/doc/api/process.md
index 38bcf878680..36e087b2cd0 100644
--- a/doc/api/process.md
+++ b/doc/api/process.md
@@ -2063,6 +2063,9 @@ Since it's not possible to build Node.js without libuv, this value is always `tr
<!-- YAML
added: v22.5.0
changes:
+ - version: REPLACEME
+ pr-url: https://github.com/nodejs/node/pull/66482
+ description: Returns a function that removes the registration.
- version: v26.12.0
pr-url: https://github.com/nodejs/node/pull/66436
description: Marked as release candidate.
@@ -2075,6 +2078,7 @@ changes:
is finalized.
* `ref` {Object | Function} The reference to the resource that is being tracked.
* `event` {string} The event that triggered the finalization. Defaults to 'exit'.
+* Returns: {Function} A function that removes this registration when called.
This function registers a callback to be called when the process emits the `exit`
event if the `ref` object was not garbage collected. If the object `ref` was garbage collected
@@ -2171,11 +2175,25 @@ but if it is not, `dispose` will be called when `process.exit` is called.
Be careful and avoid relying on this feature for the disposal of critical resources,
as it is not guaranteed that the callback will be called under all circumstances.
+The returned function removes only this registration, leaving other
+registrations for the same `ref` in place. Calling it more than once has no
+effect. It does not hold a strong reference to `ref`.
+
+```js
+const unregister = finalization.register(myDisposableObject, onFinalize);
+
+// Later, once the resource has been released manually:
+unregister();
+```
+
## `process.finalization.registerBeforeExit(ref, callback)`
<!-- YAML
added: v22.5.0
changes:
+ - version: REPLACEME
+ pr-url: https://github.com/nodejs/node/pull/66482
+ description: Returns a function that removes the registration.
- version: v26.12.0
pr-url: https://github.com/nodejs/node/pull/66436
description: Marked as release candidate.
@@ -2189,6 +2207,7 @@ changes:
is finalized.
* `ref` {Object | Function} The reference to the resource that is being tracked.
* `event` {string} The event that triggered the finalization. Defaults to 'beforeExit'.
+* Returns: {Function} A function that removes this registration when called.
This function behaves exactly like the `register`, except that the callback will be called
when the process emits the `beforeExit` event if `ref` object was not garbage collected.
diff --git a/lib/internal/process/finalization.js b/lib/internal/process/finalization.js
index 62b8cb563ba..5bca4357855 100644
--- a/lib/internal/process/finalization.js
+++ b/lib/internal/process/finalization.js
@@ -84,9 +84,19 @@ function createFinalization() {
ref.fn = fn;
registry ||= new SafeFinalizationRegistry(clear);
- registry.register(obj, ref);
+ registry.register(obj, ref, ref);
refs[event].add(ref);
+
+ // The returned function must not capture `obj`, otherwise it would
+ // never be garbage collected while the caller holds the function.
+ return function unregister() {
+ if (!refs[event].delete(ref)) {
+ return;
+ }
+ registry.unregister(ref);
+ uninstall(event);
+ };
}
/**
@@ -94,11 +104,12 @@ function createFinalization() {
* and clean things up when the object is gc.
* @param {any} obj
* @param {Function} fn
+ * @returns {Function} A function that removes this registration.
*/
function register(obj, fn) {
validateObject(obj, 'obj', kValidateObjectAllowFunction);
- _register('exit', obj, fn);
+ return _register('exit', obj, fn);
}
/**
@@ -106,11 +117,12 @@ function createFinalization() {
* and clean things up when the object is gc.
* @param {any} obj
* @param {Function} fn
+ * @returns {Function} A function that removes this registration.
*/
function registerBeforeExit(obj, fn) {
validateObject(obj, 'obj', kValidateObjectAllowFunction);
- _register('beforeExit', obj, fn);
+ return _register('beforeExit', obj, fn);
}
/**
@@ -121,16 +133,17 @@ function createFinalization() {
if (!registry) {
return;
}
- registry.unregister(obj);
for (const event of ['exit', 'beforeExit']) {
for (const ref of refs[event]) {
const _obj = ref.deref();
if (!_obj || _obj === obj) {
refs[event].delete(ref);
+ registry.unregister(ref);
}
}
- uninstall(event);
}
+ uninstall('exit');
+ uninstall('beforeExit');
}
return {
diff --git a/test/fixtures/process/unregister-function.mjs b/test/fixtures/process/unregister-function.mjs
new file mode 100644
index 00000000000..b74a5b8203a
--- /dev/null
+++ b/test/fixtures/process/unregister-function.mjs
@@ -0,0 +1,29 @@
+import { strictEqual } from 'assert'
+
+const calls = []
+
+function onExit(obj, event) {
+ calls.push(`${obj.name}:${event}`)
+}
+
+const a = { name: 'a' }
+const b = { name: 'b' }
+
+const unregisterA = process.finalization.register(a, onExit)
+const unregisterABeforeExit = process.finalization.registerBeforeExit(a, onExit)
+process.finalization.register(b, onExit)
+const unregisterBAgain = process.finalization.register(b, onExit)
+
+strictEqual(typeof unregisterA, 'function')
+strictEqual(typeof unregisterABeforeExit, 'function')
+
+unregisterA()
+unregisterA() // twice, this should not throw
+unregisterABeforeExit()
+
+// Removing one registration keeps the others for the same object.
+unregisterBAgain()
+
+process.on('exit', function () {
+ strictEqual(calls.join(','), 'b:exit')
+})
diff --git a/test/parallel/test-process-finalization.mjs b/test/parallel/test-process-finalization.mjs
index dddd0b8fae1..9daa1f178f9 100644
--- a/test/parallel/test-process-finalization.mjs
+++ b/test/parallel/test-process-finalization.mjs
@@ -12,6 +12,7 @@ const files = [
'finalization-cleanup.mjs',
'gc-not-close.mjs',
'unregister.mjs',
+ 'unregister-function.mjs',
'different-registry-per-thread.mjs',
];