mirror of
https://github.com/nodejs/node.git
synced 2025-04-29 06:19:07 +00:00

PR-URL: https://github.com/nodejs/node/pull/48528 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Santiago Gimeno <santiago.gimeno@gmail.com> Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de>
498 lines
14 KiB
JavaScript
498 lines
14 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
ArrayPrototypePush,
|
|
ArrayPrototypeShift,
|
|
Error,
|
|
ObjectPrototypeHasOwnProperty,
|
|
SafeMap,
|
|
SafeWeakMap,
|
|
} = primordials;
|
|
|
|
const {
|
|
tickInfo,
|
|
promiseRejectEvents: {
|
|
kPromiseRejectWithNoHandler,
|
|
kPromiseHandlerAddedAfterReject,
|
|
kPromiseRejectAfterResolved,
|
|
kPromiseResolveAfterResolved,
|
|
},
|
|
setPromiseRejectCallback,
|
|
} = internalBinding('task_queue');
|
|
|
|
const { deprecate } = require('internal/util');
|
|
|
|
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;
|
|
|
|
// 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
|
|
*/
|
|
|
|
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 {Array<{promise: Promise, warning: Error}>}
|
|
*/
|
|
const asyncHandledRejections = [];
|
|
|
|
/**
|
|
* @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();
|
|
}
|
|
switch (type) {
|
|
case kPromiseRejectWithNoHandler: // 0
|
|
unhandledRejection(promise, reason);
|
|
break;
|
|
case kPromiseHandlerAddedAfterReject: // 1
|
|
handledRejection(promise);
|
|
break;
|
|
case kPromiseRejectAfterResolved: // 2
|
|
if (hasMultipleResolvesListener) {
|
|
resolveErrorReject(promise, reason);
|
|
}
|
|
break;
|
|
case kPromiseResolveAfterResolved: // 3
|
|
if (hasMultipleResolvesListener) {
|
|
resolveErrorResolve(promise, reason);
|
|
}
|
|
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
|
|
* @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);
|
|
ArrayPrototypePush(asyncHandledRejections, { 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.length > 0;
|
|
|
|
while (asyncHandledRejections.length !== 0) {
|
|
const { promise, warning } = ArrayPrototypeShift(asyncHandledRejections);
|
|
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,
|
|
};
|