node/lib/internal/process/promises.js
Bradley Farias c3866b09c1 async_hooks: fix AsyncLocalStorage in unhandledRejection cases
PR-URL: https://github.com/nodejs/node/pull/41202
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Vladimir de Turckheim <vlad2t@hotmail.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: Minwoo Jung <nodecorelab@gmail.com>
2021-12-21 09:33:45 -06:00

330 lines
10 KiB
JavaScript

'use strict';
const {
ArrayPrototypePush,
ArrayPrototypeShift,
Error,
ObjectDefineProperty,
SafeWeakMap,
} = primordials;
const {
tickInfo,
promiseRejectEvents: {
kPromiseRejectWithNoHandler,
kPromiseHandlerAddedAfterReject,
kPromiseResolveAfterResolved,
kPromiseRejectAfterResolved
},
setPromiseRejectCallback
} = internalBinding('task_queue');
const {
noSideEffectsToString,
triggerUncaughtException
} = internalBinding('errors');
const {
pushAsyncContext,
popAsyncContext,
symbols: {
async_id_symbol: kAsyncIdSymbol,
trigger_async_id_symbol: kTriggerAsyncIdSymbol
}
} = require('internal/async_hooks');
const { isErrorStackTraceLimitWritable } = require('internal/errors');
// *Must* match Environment::TickInfo::Fields in src/env.h.
const kHasRejectionToWarn = 1;
const maybeUnhandledPromises = new SafeWeakMap();
const pendingUnhandledRejections = [];
const asyncHandledRejections = [];
let lastPromiseId = 0;
// --unhandled-rejections=none:
// Emit 'unhandledRejection', but do not emit any warning.
const kIgnoreUnhandledRejections = 0;
// --unhandled-rejections=warn:
// Emit 'unhandledRejection', then emit 'UnhandledPromiseRejectionWarning'.
const kAlwaysWarnUnhandledRejections = 1;
// --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'.
const kStrictUnhandledRejections = 2;
// --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.
const kThrowUnhandledRejections = 3;
// --unhandled-rejections=warn-with-error-code:
// Emit 'unhandledRejection', if it's unhandled, emit
// 'UnhandledPromiseRejectionWarning', then set process exit code to 1.
const kWarnWithErrorCodeUnhandledRejections = 4;
let unhandledRejectionsMode;
function setHasRejectionToWarn(value) {
tickInfo[kHasRejectionToWarn] = value ? 1 : 0;
}
function hasRejectionToWarn() {
return tickInfo[kHasRejectionToWarn] === 1;
}
function getUnhandledRejectionsMode() {
const { getOptionValue } = require('internal/options');
switch (getOptionValue('--unhandled-rejections')) {
case 'none':
return kIgnoreUnhandledRejections;
case 'warn':
return kAlwaysWarnUnhandledRejections;
case 'strict':
return kStrictUnhandledRejections;
case 'throw':
return kThrowUnhandledRejections;
case 'warn-with-error-code':
return kWarnWithErrorCodeUnhandledRejections;
default:
return kThrowUnhandledRejections;
}
}
function promiseRejectHandler(type, promise, reason) {
if (unhandledRejectionsMode === undefined) {
unhandledRejectionsMode = getUnhandledRejectionsMode();
}
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) {
const emit = (reason, promise, promiseInfo) => {
if (promiseInfo.domain) {
return promiseInfo.domain.emit('error', reason);
}
return process.emit('unhandledRejection', reason, promise);
};
maybeUnhandledPromises.set(promise, {
reason,
uid: ++lastPromiseId,
warned: false,
domain: process.domain,
emit
});
// This causes the promise to be referenced at least for one tick.
ArrayPrototypePush(pendingUnhandledRejections, 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;
ArrayPrototypePush(asyncHandledRejections, { promise, warning });
setHasRejectionToWarn(true);
return;
}
}
if (maybeUnhandledPromises.size === 0 && asyncHandledRejections.length === 0)
setHasRejectionToWarn(false);
}
const unhandledRejectionErrName = 'UnhandledPromiseRejectionWarning';
function emitUnhandledRejectionWarning(uid, reason) {
const warning = getErrorWithoutStack(
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(). ' +
'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})`
);
try {
if (reason instanceof Error) {
warning.stack = reason.stack;
process.emitWarning(reason.stack, unhandledRejectionErrName);
} else {
process.emitWarning(
noSideEffectsToString(reason), unhandledRejectionErrName);
}
} catch {}
process.emitWarning(warning);
}
// 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 len = pendingUnhandledRejections.length;
while (len--) {
const promise = ArrayPrototypeShift(pendingUnhandledRejections);
const promiseInfo = maybeUnhandledPromises.get(promise);
if (promiseInfo === undefined) {
continue;
}
promiseInfo.warned = true;
const { reason, uid, emit } = promiseInfo;
let needPop = true;
const {
[kAsyncIdSymbol]: promiseAsyncId,
[kTriggerAsyncIdSymbol]: promiseTriggerAsyncId,
} = promise;
// 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
if (typeof promiseAsyncId !== 'undefined') {
pushAsyncContext(
promiseAsyncId,
promiseTriggerAsyncId,
promise
);
}
try {
switch (unhandledRejectionsMode) {
case kStrictUnhandledRejections: {
const err = reason instanceof Error ?
reason : generateUnhandledRejectionError(reason);
// This destroys the async stack, don't clear it after
triggerUncaughtException(err, true /* fromPromise */);
if (typeof promiseAsyncId !== 'undefined') {
pushAsyncContext(
promise[kAsyncIdSymbol],
promise[kTriggerAsyncIdSymbol],
promise
);
}
const handled = emit(reason, promise, promiseInfo);
if (!handled) emitUnhandledRejectionWarning(uid, reason);
break;
}
case kIgnoreUnhandledRejections: {
emit(reason, promise, promiseInfo);
break;
}
case kAlwaysWarnUnhandledRejections: {
emit(reason, promise, promiseInfo);
emitUnhandledRejectionWarning(uid, reason);
break;
}
case kThrowUnhandledRejections: {
const handled = emit(reason, promise, promiseInfo);
if (!handled) {
const err = reason instanceof Error ?
reason : generateUnhandledRejectionError(reason);
// This destroys the async stack, don't clear it after
triggerUncaughtException(err, true /* fromPromise */);
needPop = false;
}
break;
}
case kWarnWithErrorCodeUnhandledRejections: {
const handled = emit(reason, promise, promiseInfo);
if (!handled) {
emitUnhandledRejectionWarning(uid, reason);
process.exitCode = 1;
}
break;
}
}
} finally {
if (needPop) {
if (typeof promiseAsyncId !== 'undefined') {
popAsyncContext(promiseAsyncId);
}
}
}
maybeScheduledTicksOrMicrotasks = true;
}
return maybeScheduledTicksOrMicrotasks ||
pendingUnhandledRejections.length !== 0;
}
function getErrorWithoutStack(name, message) {
// Reset the stack to prevent any overhead.
const tmp = Error.stackTraceLimit;
if (isErrorStackTraceLimitWritable()) Error.stackTraceLimit = 0;
// eslint-disable-next-line no-restricted-syntax
const err = new Error(message);
if (isErrorStackTraceLimitWritable()) Error.stackTraceLimit = tmp;
ObjectDefineProperty(err, 'name', {
value: name,
enumerable: false,
writable: true,
configurable: true,
});
return err;
}
function generateUnhandledRejectionError(reason) {
const message =
'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)}".`;
const err = getErrorWithoutStack('UnhandledPromiseRejection', message);
err.code = 'ERR_UNHANDLED_REJECTION';
return err;
}
function listenForRejections() {
setPromiseRejectCallback(promiseRejectHandler);
}
module.exports = {
hasRejectionToWarn,
setHasRejectionToWarn,
listenForRejections,
processPromiseRejections,
};