process: move multipleResolves event to EOL

The `multipleResolves` event has been deprecated for several
years now. It's time.

PR-URL: https://github.com/nodejs/node/pull/58707
Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
This commit is contained in:
James M Snell 2025-06-14 14:49:17 -07:00
parent 4d5ee2491b
commit a3dfca90d1
9 changed files with 10 additions and 238 deletions

View File

@ -3334,6 +3334,9 @@ the errors used for value type validation.
<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/58707
description: End-of-Life.
- version: v18.0.0
pr-url: https://github.com/nodejs/node/pull/41896
description: Runtime deprecation.
@ -3344,10 +3347,10 @@ changes:
description: Documentation-only deprecation.
-->
Type: Runtime
Type: End-of-Life
This event was deprecated because it did not work with V8 promise combinators
which diminished its usefulness.
This event was deprecated and removed because it did not work with V8 promise
combinators which diminished its usefulness.
### DEP0161: `process._getActiveRequests()` and `process._getActiveHandles()`

View File

@ -176,95 +176,6 @@ process, the `message` argument can contain data that JSON is not able
to represent.
See [Advanced serialization for `child_process`][] for more details.
### Event: `'multipleResolves'`
<!-- YAML
added: v10.12.0
deprecated:
- v17.6.0
- v16.15.0
-->
> Stability: 0 - Deprecated
* `type` {string} The resolution type. One of `'resolve'` or `'reject'`.
* `promise` {Promise} The promise that resolved or rejected more than once.
* `value` {any} The value with which the promise was either resolved or
rejected after the original resolve.
The `'multipleResolves'` event is emitted whenever a `Promise` has been either:
* Resolved more than once.
* Rejected more than once.
* Rejected after resolve.
* Resolved after reject.
This is useful for tracking potential errors in an application while using the
`Promise` constructor, as multiple resolutions are silently swallowed. However,
the occurrence of this event does not necessarily indicate an error. For
example, [`Promise.race()`][] can trigger a `'multipleResolves'` event.
Because of the unreliability of the event in cases like the
[`Promise.race()`][] example above it has been deprecated.
```mjs
import process from 'node:process';
process.on('multipleResolves', (type, promise, reason) => {
console.error(type, promise, reason);
setImmediate(() => process.exit(1));
});
async function main() {
try {
return await new Promise((resolve, reject) => {
resolve('First call');
resolve('Swallowed resolve');
reject(new Error('Swallowed reject'));
});
} catch {
throw new Error('Failed');
}
}
main().then(console.log);
// resolve: Promise { 'First call' } 'Swallowed resolve'
// reject: Promise { 'First call' } Error: Swallowed reject
// at Promise (*)
// at new Promise (<anonymous>)
// at main (*)
// First call
```
```cjs
const process = require('node:process');
process.on('multipleResolves', (type, promise, reason) => {
console.error(type, promise, reason);
setImmediate(() => process.exit(1));
});
async function main() {
try {
return await new Promise((resolve, reject) => {
resolve('First call');
resolve('Swallowed resolve');
reject(new Error('Swallowed reject'));
});
} catch {
throw new Error('Failed');
}
}
main().then(console.log);
// resolve: Promise { 'First call' } 'Swallowed resolve'
// reject: Promise { 'First call' } Error: Swallowed reject
// at Promise (*)
// at new Promise (<anonymous>)
// at main (*)
// First call
```
### Event: `'rejectionHandled'`
<!-- YAML
@ -4603,7 +4514,6 @@ cases:
[`Error`]: errors.md#class-error
[`EventEmitter`]: events.md#class-eventemitter
[`NODE_OPTIONS`]: cli.md#node_optionsoptions
[`Promise.race()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
[`Worker`]: worker_threads.md#class-worker
[`Worker` constructor]: worker_threads.md#new-workerfilename-options
[`console.error()`]: console.md#consoleerrordata-args

View File

@ -20,8 +20,6 @@ const {
setPromiseRejectCallback,
} = internalBinding('task_queue');
const { deprecate } = require('internal/util');
const {
noSideEffectsToString,
triggerUncaughtException,
@ -43,27 +41,6 @@ const AsyncContextFrame = require('internal/async_context_frame');
// *Must* match Environment::TickInfo::Fields in src/env.h.
const kHasRejectionToWarn = 1;
// By default true because in cases where process is not a global
// it is not possible to determine if the user has added a listener
// to the process object.
let hasMultipleResolvesListener = true;
if (process.on) {
hasMultipleResolvesListener = process.listenerCount('multipleResolves') !== 0;
process.on('newListener', (eventName) => {
if (eventName === 'multipleResolves') {
hasMultipleResolvesListener = true;
}
});
process.on('removeListener', (eventName) => {
if (eventName === 'multipleResolves') {
hasMultipleResolvesListener = process.listenerCount('multipleResolves') !== 0;
}
});
}
/**
* Errors & Warnings
*/
@ -192,55 +169,16 @@ function promiseRejectHandler(type, promise, reason) {
handledRejection(promise);
break;
case kPromiseRejectAfterResolved: // 2
if (hasMultipleResolvesListener) {
resolveErrorReject(promise, reason);
}
// Do nothing in this case. Previous we would emit a multipleResolves
// event but that was deprecated then later removed.
break;
case kPromiseResolveAfterResolved: // 3
if (hasMultipleResolvesListener) {
resolveErrorResolve(promise, reason);
}
// Do nothing in this case. Previous we would emit a multipleResolves
// event but that was deprecated then later removed.
break;
}
}
const multipleResolvesDeprecate = deprecate(
() => {},
'The multipleResolves event has been deprecated.',
'DEP0160',
);
/**
* @param {Promise} promise
* @param {Error} reason
*/
function resolveErrorResolve(promise, reason) {
// We have to wrap this in a next tick. Otherwise the error could be caught by
// the executed promise.
process.nextTick(() => {
// Emit the multipleResolves event.
// This is a deprecated event, so we have to check if it's being listened to.
if (process.emit('multipleResolves', 'resolve', promise, reason)) {
// If the event is being listened to, emit a deprecation warning.
multipleResolvesDeprecate();
}
});
}
/**
* @param {Promise} promise
* @param {Error} reason
*/
function resolveErrorReject(promise, reason) {
// We have to wrap this in a next tick. Otherwise the error could be caught by
// the executed promise.
process.nextTick(() => {
if (process.emit('multipleResolves', 'reject', promise, reason)) {
multipleResolvesDeprecate();
}
});
}
/**
* @param {Promise} promise
* @param {PromiseInfo} promiseInfo

View File

@ -104,7 +104,6 @@ async function stopListeningAfterCatchingError() {
} catch (_e) {
err = _e;
}
process.removeAllListeners('multipleResolves');
strictEqual(err, expected);
strictEqual(ee.listenerCount('error'), 0);
strictEqual(ee.listenerCount('myevent'), 0);

View File

@ -1,58 +0,0 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const rejection = new Error('Swallowed reject');
const rejection2 = new TypeError('Weird');
const resolveMessage = 'First call';
const rejectPromise = new Promise((r) => setTimeout(r, 10, rejection2));
const swallowedResolve = 'Swallowed resolve';
const swallowedResolve2 = 'Foobar';
process.on('multipleResolves', common.mustCall(handler, 4));
const p1 = new Promise((resolve, reject) => {
resolve(resolveMessage);
resolve(swallowedResolve);
reject(rejection);
});
const p2 = new Promise((resolve, reject) => {
reject(rejectPromise);
resolve(swallowedResolve2);
reject(rejection2);
}).catch(common.mustCall((exception) => {
assert.strictEqual(exception, rejectPromise);
}));
const expected = [
'resolve',
p1,
swallowedResolve,
'reject',
p1,
rejection,
'resolve',
p2,
swallowedResolve2,
'reject',
p2,
rejection2,
];
let count = 0;
function handler(type, promise, reason) {
assert.strictEqual(type, expected.shift());
// In the first two cases the promise is identical because it's not delayed.
// The other two cases are not identical, because the `promise` is caught in a
// state when it has no knowledge about the `.catch()` handler that is
// attached to it right afterwards.
if (count++ < 2) {
assert.strictEqual(promise, expected.shift());
} else {
assert.notStrictEqual(promise, expected.shift());
}
assert.strictEqual(reason, expected.shift());
}

View File

@ -14,8 +14,6 @@ const setPromiseImmediate = promisify(timers.setImmediate);
assert.strictEqual(setPromiseImmediate, timerPromises.setImmediate);
process.on('multipleResolves', common.mustNotCall());
{
const promise = setPromiseImmediate();
promise.then(common.mustCall((value) => {

View File

@ -14,8 +14,6 @@ const setPromiseTimeout = promisify(timers.setTimeout);
const { setInterval } = timerPromises;
process.on('multipleResolves', common.mustNotCall());
{
const iterable = setInterval(1, undefined);
const iterator = iterable[Symbol.asyncIterator]();

View File

@ -14,8 +14,6 @@ const setPromiseTimeout = promisify(timers.setTimeout);
assert.strictEqual(setPromiseTimeout, timerPromises.setTimeout);
process.on('multipleResolves', common.mustNotCall());
{
const promise = setPromiseTimeout(1);
promise.then(common.mustCall((value) => {

View File

@ -1,14 +0,0 @@
import { expectWarning, mustCall } from '../common/index.mjs';
expectWarning(
'DeprecationWarning',
'The multipleResolves event has been deprecated.',
'DEP0160',
);
process.on('multipleResolves', mustCall());
new Promise((resolve) => {
resolve();
resolve();
});