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',
 ];