mirror of
https://github.com/nodejs/node.git
synced 2025-05-01 17:03:34 +00:00

This adds a flag to define the default behavior for unhandled rejections. Three modes exist: `none`, `warn` and `strict`. The first is going to silence all unhandled rejection warnings. The second behaves identical to the current default with the excetion that no deprecation warning will be printed and the last is going to throw an error for each unhandled rejection, just as regular exceptions do. It is possible to intercept those with the `uncaughtException` hook as with all other exceptions as well. This PR has no influence on the existing `unhandledRejection` hook. If that is used, it will continue to function as before. PR-URL: https://github.com/nodejs/node/pull/26599 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Matheus Marchini <mat@mmarchini.me> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com> Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com> Reviewed-By: Sakthipriyan Vairamani <thechargingvolcano@gmail.com>
218 lines
6.1 KiB
JavaScript
218 lines
6.1 KiB
JavaScript
'use strict';
|
|
|
|
const { Object } = primordials;
|
|
|
|
const {
|
|
safeToString
|
|
} = internalBinding('util');
|
|
const {
|
|
tickInfo,
|
|
promiseRejectEvents: {
|
|
kPromiseRejectWithNoHandler,
|
|
kPromiseHandlerAddedAfterReject,
|
|
kPromiseResolveAfterResolved,
|
|
kPromiseRejectAfterResolved
|
|
},
|
|
setPromiseRejectCallback,
|
|
triggerFatalException
|
|
} = internalBinding('task_queue');
|
|
|
|
// *Must* match Environment::TickInfo::Fields in src/env.h.
|
|
const kHasRejectionToWarn = 1;
|
|
|
|
const maybeUnhandledPromises = new WeakMap();
|
|
const pendingUnhandledRejections = [];
|
|
const asyncHandledRejections = [];
|
|
let lastPromiseId = 0;
|
|
|
|
const states = {
|
|
none: 0,
|
|
warn: 1,
|
|
strict: 2,
|
|
default: 3
|
|
};
|
|
|
|
let state;
|
|
|
|
function setHasRejectionToWarn(value) {
|
|
tickInfo[kHasRejectionToWarn] = value ? 1 : 0;
|
|
}
|
|
|
|
function hasRejectionToWarn() {
|
|
return tickInfo[kHasRejectionToWarn] === 1;
|
|
}
|
|
|
|
function promiseRejectHandler(type, promise, reason) {
|
|
if (state === undefined) {
|
|
const { getOptionValue } = require('internal/options');
|
|
state = states[getOptionValue('--unhandled-rejections') || 'default'];
|
|
}
|
|
switch (type) {
|
|
case kPromiseRejectWithNoHandler:
|
|
unhandledRejection(promise, reason);
|
|
break;
|
|
case kPromiseHandlerAddedAfterReject:
|
|
handledRejection(promise);
|
|
break;
|
|
case kPromiseResolveAfterResolved:
|
|
resolveError('resolve', promise, reason);
|
|
break;
|
|
case kPromiseRejectAfterResolved:
|
|
resolveError('reject', promise, reason);
|
|
break;
|
|
}
|
|
}
|
|
|
|
function resolveError(type, promise, reason) {
|
|
// We have to wrap this in a next tick. Otherwise the error could be caught by
|
|
// the executed promise.
|
|
process.nextTick(() => {
|
|
process.emit('multipleResolves', type, promise, reason);
|
|
});
|
|
}
|
|
|
|
function unhandledRejection(promise, reason) {
|
|
maybeUnhandledPromises.set(promise, {
|
|
reason,
|
|
uid: ++lastPromiseId,
|
|
warned: false
|
|
});
|
|
// This causes the promise to be referenced at least for one tick.
|
|
pendingUnhandledRejections.push(promise);
|
|
setHasRejectionToWarn(true);
|
|
}
|
|
|
|
function handledRejection(promise) {
|
|
const promiseInfo = maybeUnhandledPromises.get(promise);
|
|
if (promiseInfo !== undefined) {
|
|
maybeUnhandledPromises.delete(promise);
|
|
if (promiseInfo.warned) {
|
|
const { uid } = promiseInfo;
|
|
// Generate the warning object early to get a good stack trace.
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
const warning = new Error('Promise rejection was handled ' +
|
|
`asynchronously (rejection id: ${uid})`);
|
|
warning.name = 'PromiseRejectionHandledWarning';
|
|
warning.id = uid;
|
|
asyncHandledRejections.push({ promise, warning });
|
|
setHasRejectionToWarn(true);
|
|
return;
|
|
}
|
|
}
|
|
setHasRejectionToWarn(false);
|
|
}
|
|
|
|
const unhandledRejectionErrName = 'UnhandledPromiseRejectionWarning';
|
|
function emitWarning(uid, reason) {
|
|
if (state === states.none) {
|
|
return;
|
|
}
|
|
const warning = getError(
|
|
unhandledRejectionErrName,
|
|
'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(). ' +
|
|
`(rejection id: ${uid})`
|
|
);
|
|
try {
|
|
if (reason instanceof Error) {
|
|
warning.stack = reason.stack;
|
|
process.emitWarning(reason.stack, unhandledRejectionErrName);
|
|
} else {
|
|
process.emitWarning(safeToString(reason), unhandledRejectionErrName);
|
|
}
|
|
} catch {}
|
|
|
|
process.emitWarning(warning);
|
|
emitDeprecationWarning();
|
|
}
|
|
|
|
let deprecationWarned = false;
|
|
function emitDeprecationWarning() {
|
|
if (state === states.default && !deprecationWarned) {
|
|
deprecationWarned = true;
|
|
process.emitWarning(
|
|
'Unhandled promise rejections are deprecated. In the future, ' +
|
|
'promise rejections that are not handled will terminate the ' +
|
|
'Node.js process with a non-zero exit code.',
|
|
'DeprecationWarning', 'DEP0018');
|
|
}
|
|
}
|
|
|
|
// If this method returns true, at least one more tick need to be
|
|
// scheduled to process any potential pending rejections
|
|
function processPromiseRejections() {
|
|
while (asyncHandledRejections.length > 0) {
|
|
const { promise, warning } = asyncHandledRejections.shift();
|
|
if (!process.emit('rejectionHandled', promise)) {
|
|
process.emitWarning(warning);
|
|
}
|
|
}
|
|
|
|
let maybeScheduledTicks = false;
|
|
let len = pendingUnhandledRejections.length;
|
|
while (len--) {
|
|
const promise = pendingUnhandledRejections.shift();
|
|
const promiseInfo = maybeUnhandledPromises.get(promise);
|
|
if (promiseInfo === undefined) {
|
|
continue;
|
|
}
|
|
promiseInfo.warned = true;
|
|
const { reason, uid } = promiseInfo;
|
|
if (state === states.strict) {
|
|
fatalException(reason);
|
|
}
|
|
if (!process.emit('unhandledRejection', reason, promise) ||
|
|
// Always warn in case the user requested it.
|
|
state === states.warn) {
|
|
emitWarning(uid, reason);
|
|
}
|
|
maybeScheduledTicks = true;
|
|
}
|
|
return maybeScheduledTicks || pendingUnhandledRejections.length !== 0;
|
|
}
|
|
|
|
function getError(name, message) {
|
|
// Reset the stack to prevent any overhead.
|
|
const tmp = Error.stackTraceLimit;
|
|
Error.stackTraceLimit = 0;
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
const err = new Error(message);
|
|
Error.stackTraceLimit = tmp;
|
|
Object.defineProperty(err, 'name', {
|
|
value: name,
|
|
enumerable: false,
|
|
writable: true,
|
|
configurable: true,
|
|
});
|
|
return err;
|
|
}
|
|
|
|
function fatalException(reason) {
|
|
let err;
|
|
if (reason instanceof Error) {
|
|
err = reason;
|
|
} else {
|
|
err = getError(
|
|
'UnhandledPromiseRejection',
|
|
'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 "${safeToString(reason)}".`
|
|
);
|
|
err.code = 'ERR_UNHANDLED_REJECTION';
|
|
}
|
|
triggerFatalException(err, true /* fromPromise */);
|
|
}
|
|
|
|
function listenForRejections() {
|
|
setPromiseRejectCallback(promiseRejectHandler);
|
|
}
|
|
|
|
module.exports = {
|
|
hasRejectionToWarn,
|
|
setHasRejectionToWarn,
|
|
listenForRejections,
|
|
processPromiseRejections
|
|
};
|