When Promise.race() or Promise.any() settles, V8 fires kPromiseResolveAfterResolved / kPromiseRejectAfterResolved for each "losing" promise. The PromiseRejectCallback in node_task_queue.cc was crossing into JS for these events, but since the multipleResolves event reached EOL in v25 (PR #58707), the JS handler does nothing. The unnecessary C++-to-JS boundary crossings accumulate references in a tight loop, causing OOM when using Promise.race() with immediately-resolving promises. Return early in PromiseRejectCallback() for these two events, skipping the JS callback entirely. Also remove the dead case branches and unused constant imports from the JS side. Move early returns for kPromiseResolveAfterResolved and kPromiseRejectAfterResolved before Number::New and CHECK(!callback), avoiding unnecessary work. Remove dead NODE_DEFINE_CONSTANT exports and fix comment placement in the JS switch statement. Bump test --max-old-space-size from 20 to 64 for safety on instrumented builds. Fixes: https://github.com/nodejs/node/issues/51452 Refs: https://github.com/nodejs/node/pull/60184 Refs: https://github.com/nodejs/node/pull/61960 PR-URL: https://github.com/nodejs/node/pull/62336 Refs: https://github.com/nodejs/node/issues/51452 Reviewed-By: Jordan Harband <ljharb@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
427 lines
12 KiB
JavaScript
427 lines
12 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
Error,
|
|
ObjectPrototypeHasOwnProperty,
|
|
SafeMap,
|
|
SafeWeakMap,
|
|
} = primordials;
|
|
|
|
const FixedQueue = require('internal/fixed_queue');
|
|
|
|
const {
|
|
tickInfo,
|
|
promiseRejectEvents: {
|
|
kPromiseRejectWithNoHandler,
|
|
kPromiseHandlerAddedAfterReject,
|
|
},
|
|
setPromiseRejectCallback,
|
|
} = internalBinding('task_queue');
|
|
|
|
const {
|
|
noSideEffectsToString,
|
|
triggerUncaughtException,
|
|
exitCodes: { kGenericUserError },
|
|
} = internalBinding('errors');
|
|
|
|
const {
|
|
pushAsyncContext,
|
|
popAsyncContext,
|
|
symbols: {
|
|
async_id_symbol: kAsyncIdSymbol,
|
|
trigger_async_id_symbol: kTriggerAsyncIdSymbol,
|
|
},
|
|
} = require('internal/async_hooks');
|
|
const { isErrorStackTraceLimitWritable } = require('internal/errors');
|
|
|
|
const AsyncContextFrame = require('internal/async_context_frame');
|
|
|
|
// *Must* match Environment::TickInfo::Fields in src/env.h.
|
|
const kHasRejectionToWarn = 1;
|
|
|
|
/**
|
|
* Errors & Warnings
|
|
*/
|
|
|
|
class UnhandledPromiseRejection extends Error {
|
|
code = 'ERR_UNHANDLED_REJECTION';
|
|
name = 'UnhandledPromiseRejection';
|
|
/**
|
|
* @param {Error} reason
|
|
*/
|
|
constructor(reason) {
|
|
super('This error originated either by throwing inside of an ' +
|
|
'async function without a catch block, or by rejecting a promise which ' +
|
|
'was not handled with .catch(). The promise rejected with the reason "' +
|
|
noSideEffectsToString(reason) + '".');
|
|
}
|
|
}
|
|
|
|
class UnhandledPromiseRejectionWarning extends Error {
|
|
name = 'UnhandledPromiseRejectionWarning';
|
|
/**
|
|
* @param {number} uid
|
|
*/
|
|
constructor(uid) {
|
|
const message = 'Unhandled promise rejection. This error originated either by ' +
|
|
'throwing inside of an async function without a catch block, ' +
|
|
'or by rejecting a promise which was not handled with .catch(). ' +
|
|
'To terminate the node process on unhandled promise ' +
|
|
'rejection, use the CLI flag `--unhandled-rejections=strict` (see ' +
|
|
'https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). ' +
|
|
`(rejection id: ${uid})`;
|
|
|
|
// UnhandledPromiseRejectionWarning will get the stack trace from the
|
|
// reason, so we can disable the stack trace limit temporarily for better
|
|
// performance.
|
|
if (isErrorStackTraceLimitWritable()) {
|
|
const stackTraceLimit = Error.stackTraceLimit;
|
|
Error.stackTraceLimit = 0;
|
|
super(message);
|
|
Error.stackTraceLimit = stackTraceLimit;
|
|
} else {
|
|
super(message);
|
|
}
|
|
}
|
|
}
|
|
|
|
class PromiseRejectionHandledWarning extends Error {
|
|
name = 'PromiseRejectionHandledWarning';
|
|
|
|
/**
|
|
* @param {number} uid
|
|
*/
|
|
constructor(uid) {
|
|
super(`Promise rejection was handled asynchronously (rejection id: ${uid})`);
|
|
this.id = uid;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @typedef PromiseInfo
|
|
* @property {*} reason the reason for the rejection
|
|
* @property {number} uid the unique id of the promise
|
|
* @property {boolean} warned whether the rejection has been warned
|
|
* @property {object} [domain] the domain the promise was created in
|
|
*/
|
|
|
|
/**
|
|
* @type {WeakMap<Promise, PromiseInfo>}
|
|
*/
|
|
const maybeUnhandledPromises = new SafeWeakMap();
|
|
|
|
/**
|
|
* Using a Mp causes the promise to be referenced at least for one tick.
|
|
* @type {Map<Promise, PromiseInfo>}
|
|
*/
|
|
let pendingUnhandledRejections = new SafeMap();
|
|
|
|
/**
|
|
* @type {import('internal/fixed_queue')<{promise: Promise, warning: Error}>}
|
|
*/
|
|
const asyncHandledRejections = new FixedQueue();
|
|
|
|
/**
|
|
* @type {number}
|
|
*/
|
|
let lastPromiseId = 0;
|
|
|
|
/**
|
|
* @param {boolean} value
|
|
*/
|
|
function setHasRejectionToWarn(value) {
|
|
tickInfo[kHasRejectionToWarn] = value ? 1 : 0;
|
|
}
|
|
|
|
/**
|
|
* @returns {boolean}
|
|
*/
|
|
function hasRejectionToWarn() {
|
|
return tickInfo[kHasRejectionToWarn] === 1;
|
|
}
|
|
|
|
/**
|
|
* @param {string|Error} obj
|
|
* @returns {obj is Error}
|
|
*/
|
|
function isErrorLike(obj) {
|
|
return typeof obj === 'object' &&
|
|
obj !== null &&
|
|
ObjectPrototypeHasOwnProperty(obj, 'stack');
|
|
}
|
|
|
|
/**
|
|
* @param {0|1|2|3} type
|
|
* @param {Promise} promise
|
|
* @param {Error} reason
|
|
*/
|
|
function promiseRejectHandler(type, promise, reason) {
|
|
if (unhandledRejectionsMode === undefined) {
|
|
unhandledRejectionsMode = getUnhandledRejectionsMode();
|
|
}
|
|
// kPromiseRejectAfterResolved and kPromiseResolveAfterResolved are
|
|
// filtered out in C++ (src/node_task_queue.cc) and never reach JS.
|
|
switch (type) {
|
|
case kPromiseRejectWithNoHandler: // 0
|
|
unhandledRejection(promise, reason);
|
|
break;
|
|
case kPromiseHandlerAddedAfterReject: // 1
|
|
handledRejection(promise);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Promise} promise
|
|
* @param {PromiseInfo} promiseInfo
|
|
* @returns {boolean}
|
|
*/
|
|
const emitUnhandledRejection = (promise, promiseInfo) => {
|
|
return promiseInfo.domain ?
|
|
promiseInfo.domain.emit('error', promiseInfo.reason) :
|
|
process.emit('unhandledRejection', promiseInfo.reason, promise);
|
|
};
|
|
|
|
/**
|
|
* @param {Promise} promise
|
|
* @param {Error} reason
|
|
*/
|
|
function unhandledRejection(promise, reason) {
|
|
pendingUnhandledRejections.set(promise, {
|
|
reason,
|
|
uid: ++lastPromiseId,
|
|
warned: false,
|
|
domain: process.domain,
|
|
contextFrame: AsyncContextFrame.current(),
|
|
});
|
|
setHasRejectionToWarn(true);
|
|
}
|
|
|
|
/**
|
|
* @param {Promise} promise
|
|
*/
|
|
function handledRejection(promise) {
|
|
if (pendingUnhandledRejections.has(promise)) {
|
|
pendingUnhandledRejections.delete(promise);
|
|
return;
|
|
}
|
|
const promiseInfo = maybeUnhandledPromises.get(promise);
|
|
if (promiseInfo !== undefined) {
|
|
maybeUnhandledPromises.delete(promise);
|
|
if (promiseInfo.warned) {
|
|
// Generate the warning object early to get a good stack trace.
|
|
const warning = new PromiseRejectionHandledWarning(promiseInfo.uid);
|
|
asyncHandledRejections.push({ promise, warning });
|
|
setHasRejectionToWarn(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
const unhandledRejectionErrName = UnhandledPromiseRejectionWarning.name;
|
|
|
|
/**
|
|
* @param {PromiseInfo} promiseInfo
|
|
*/
|
|
function emitUnhandledRejectionWarning(promiseInfo) {
|
|
const warning = new UnhandledPromiseRejectionWarning(promiseInfo.uid);
|
|
const reason = promiseInfo.reason;
|
|
try {
|
|
if (isErrorLike(reason)) {
|
|
warning.stack = reason.stack;
|
|
process.emitWarning(reason.stack, unhandledRejectionErrName);
|
|
} else {
|
|
process.emitWarning(
|
|
noSideEffectsToString(reason), unhandledRejectionErrName);
|
|
}
|
|
} catch {
|
|
try {
|
|
process.emitWarning(
|
|
noSideEffectsToString(reason), unhandledRejectionErrName);
|
|
} catch {
|
|
// Ignore.
|
|
}
|
|
}
|
|
|
|
process.emitWarning(warning);
|
|
}
|
|
|
|
/**
|
|
* @callback UnhandledRejectionsModeHandler
|
|
* @param {Promise} promise
|
|
* @param {PromiseInfo} promiseInfo
|
|
* @param {number} [promiseAsyncId]
|
|
* @returns {boolean}
|
|
*/
|
|
|
|
/**
|
|
* The mode of unhandled rejections.
|
|
* @type {UnhandledRejectionsModeHandler}
|
|
*/
|
|
let unhandledRejectionsMode;
|
|
|
|
/**
|
|
* --unhandled-rejections=strict:
|
|
* Emit 'uncaughtException'. If it's not handled, print the error to stderr
|
|
* and exit the process.
|
|
* Otherwise, emit 'unhandledRejection'. If 'unhandledRejection' is not
|
|
* handled, emit 'UnhandledPromiseRejectionWarning'.
|
|
* @type {UnhandledRejectionsModeHandler}
|
|
*/
|
|
function strictUnhandledRejectionsMode(promise, promiseInfo, promiseAsyncId) {
|
|
const reason = promiseInfo.reason;
|
|
const err = isErrorLike(reason) ?
|
|
reason : new UnhandledPromiseRejection(reason);
|
|
// This destroys the async stack, don't clear it after
|
|
triggerUncaughtException(err, true /* fromPromise */);
|
|
if (promiseAsyncId !== undefined) {
|
|
pushAsyncContext(
|
|
promise[kAsyncIdSymbol],
|
|
promise[kTriggerAsyncIdSymbol],
|
|
promise,
|
|
);
|
|
}
|
|
const handled = emitUnhandledRejection(promise, promiseInfo);
|
|
if (!handled) emitUnhandledRejectionWarning(promiseInfo);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* --unhandled-rejections=none:
|
|
* Emit 'unhandledRejection', but do not emit any warning.
|
|
* @type {UnhandledRejectionsModeHandler}
|
|
*/
|
|
function ignoreUnhandledRejectionsMode(promise, promiseInfo) {
|
|
emitUnhandledRejection(promise, promiseInfo);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* --unhandled-rejections=warn:
|
|
* Emit 'unhandledRejection', then emit 'UnhandledPromiseRejectionWarning'.
|
|
* @type {UnhandledRejectionsModeHandler}
|
|
*/
|
|
function alwaysWarnUnhandledRejectionsMode(promise, promiseInfo) {
|
|
emitUnhandledRejection(promise, promiseInfo);
|
|
emitUnhandledRejectionWarning(promiseInfo);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* --unhandled-rejections=throw:
|
|
* Emit 'unhandledRejection', if it's unhandled, emit
|
|
* 'uncaughtException'. If it's not handled, print the error to stderr
|
|
* and exit the process.
|
|
* @type {UnhandledRejectionsModeHandler}
|
|
*/
|
|
function throwUnhandledRejectionsMode(promise, promiseInfo) {
|
|
const reason = promiseInfo.reason;
|
|
const handled = emitUnhandledRejection(promise, promiseInfo);
|
|
if (!handled) {
|
|
const err = isErrorLike(reason) ?
|
|
reason :
|
|
new UnhandledPromiseRejection(reason);
|
|
// This destroys the async stack, don't clear it after
|
|
triggerUncaughtException(err, true /* fromPromise */);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* --unhandled-rejections=warn-with-error-code:
|
|
* Emit 'unhandledRejection', if it's unhandled, emit
|
|
* 'UnhandledPromiseRejectionWarning', then set process exit code to 1.
|
|
* @type {UnhandledRejectionsModeHandler}
|
|
*/
|
|
function warnWithErrorCodeUnhandledRejectionsMode(promise, promiseInfo) {
|
|
const handled = emitUnhandledRejection(promise, promiseInfo);
|
|
if (!handled) {
|
|
emitUnhandledRejectionWarning(promiseInfo);
|
|
process.exitCode = kGenericUserError;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @returns {UnhandledRejectionsModeHandler}
|
|
*/
|
|
function getUnhandledRejectionsMode() {
|
|
const { getOptionValue } = require('internal/options');
|
|
switch (getOptionValue('--unhandled-rejections')) {
|
|
case 'none':
|
|
return ignoreUnhandledRejectionsMode;
|
|
case 'warn':
|
|
return alwaysWarnUnhandledRejectionsMode;
|
|
case 'strict':
|
|
return strictUnhandledRejectionsMode;
|
|
case 'throw':
|
|
return throwUnhandledRejectionsMode;
|
|
case 'warn-with-error-code':
|
|
return warnWithErrorCodeUnhandledRejectionsMode;
|
|
default:
|
|
return throwUnhandledRejectionsMode;
|
|
}
|
|
}
|
|
|
|
// If this method returns true, we've executed user code or triggered
|
|
// a warning to be emitted which requires the microtask and next tick
|
|
// queues to be drained again.
|
|
function processPromiseRejections() {
|
|
let maybeScheduledTicksOrMicrotasks = !asyncHandledRejections.isEmpty();
|
|
|
|
while (!asyncHandledRejections.isEmpty()) {
|
|
const { promise, warning } = asyncHandledRejections.shift();
|
|
if (!process.emit('rejectionHandled', promise)) {
|
|
process.emitWarning(warning);
|
|
}
|
|
}
|
|
|
|
let needPop = true;
|
|
let promiseAsyncId;
|
|
|
|
const pending = pendingUnhandledRejections;
|
|
pendingUnhandledRejections = new SafeMap();
|
|
|
|
for (const { 0: promise, 1: promiseInfo } of pending.entries()) {
|
|
maybeUnhandledPromises.set(promise, promiseInfo);
|
|
|
|
promiseInfo.warned = true;
|
|
|
|
// We need to check if async_hooks are enabled
|
|
// don't use enabledHooksExist as a Promise could
|
|
// come from a vm.* context and not have an async id
|
|
promiseAsyncId = promise[kAsyncIdSymbol];
|
|
if (promiseAsyncId !== undefined) {
|
|
pushAsyncContext(
|
|
promiseAsyncId,
|
|
promise[kTriggerAsyncIdSymbol],
|
|
promise,
|
|
);
|
|
}
|
|
|
|
const { contextFrame } = promiseInfo;
|
|
const priorContextFrame = AsyncContextFrame.exchange(contextFrame);
|
|
try {
|
|
needPop = unhandledRejectionsMode(promise, promiseInfo, promiseAsyncId);
|
|
} finally {
|
|
AsyncContextFrame.set(priorContextFrame);
|
|
needPop &&
|
|
promiseAsyncId !== undefined &&
|
|
popAsyncContext(promiseAsyncId);
|
|
}
|
|
maybeScheduledTicksOrMicrotasks = true;
|
|
}
|
|
return maybeScheduledTicksOrMicrotasks ||
|
|
pendingUnhandledRejections.size !== 0;
|
|
}
|
|
|
|
function listenForRejections() {
|
|
setPromiseRejectCallback(promiseRejectHandler);
|
|
}
|
|
|
|
module.exports = {
|
|
hasRejectionToWarn,
|
|
setHasRejectionToWarn,
|
|
listenForRejections,
|
|
processPromiseRejections,
|
|
};
|