Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

process: refactor unhandled rejection handling #28228

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 57 additions & 22 deletions lib/internal/process/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,24 @@ const pendingUnhandledRejections = [];
const asyncHandledRejections = [];
let lastPromiseId = 0;

const states = {
none: 0,
warn: 1,
strict: 2,
default: 3
};

let state;
// --unhandled-rejection=none:
// Emit 'unhandledRejection', but do not emit any warning.
const kIgnoreUnhandledRejections = 0;
// --unhandled-rejection=warn:
// Emit 'unhandledRejection', then emit 'UnhandledPromiseRejectionWarning'.
const kAlwaysWarnUnhandledRejections = 1;
// --unhandled-rejection=strict:
// Emit 'uncaughtException'. If it's not handled, print the error to stderr
// and exit the process.
// Otherwise, emit 'unhandledRejection'. If 'unhandledRejection' is not
Copy link
Member Author

@joyeecheung joyeecheung Jun 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH this behavior (currently on master) looks weird to me. It contradicts with the description in test/parallel/test-promise-unhandled-error.js:

// Verify that unhandled rejections always trigger uncaught exceptions instead
// of triggering unhandled rejections.

Even though that test specifically tests:

process.on('unhandledRejection', common.mustCall(2));

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test description is faulty. The original implementation changed multiple times and it seems like the description was not updated appropriately.

The original intention was to replace the unhandled rejections with the exception. Later on it was decided that the current unhandled rejections hook behavior should not be influenced by the flag at all.

// handled, emit 'UnhandledPromiseRejectionWarning'.
const kThrowUnhandledRejections = 2;
// --unhandled-rejection is unset:
// Emit 'unhandledRejection', if it's handled, emit
// 'UnhandledPromiseRejectionWarning', then emit deprecation warning.
const kDefaultUnhandledRejections = 3;

let unhandledRejectionsMode;

function setHasRejectionToWarn(value) {
tickInfo[kHasRejectionToWarn] = value ? 1 : 0;
Expand All @@ -42,10 +52,23 @@ 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 kThrowUnhandledRejections;
default:
return kDefaultUnhandledRejections;
}
}

function promiseRejectHandler(type, promise, reason) {
if (state === undefined) {
const { getOptionValue } = require('internal/options');
state = states[getOptionValue('--unhandled-rejections') || 'default'];
if (unhandledRejectionsMode === undefined) {
unhandledRejectionsMode = getUnhandledRejectionsMode();
}
switch (type) {
case kPromiseRejectWithNoHandler:
Expand Down Expand Up @@ -104,9 +127,6 @@ function handledRejection(promise) {

const unhandledRejectionErrName = 'UnhandledPromiseRejectionWarning';
function emitWarning(uid, reason) {
if (state === states.none) {
return;
}
const warning = getError(
unhandledRejectionErrName,
'Unhandled promise rejection. This error originated either by ' +
Expand All @@ -129,7 +149,8 @@ function emitWarning(uid, reason) {

let deprecationWarned = false;
function emitDeprecationWarning() {
if (state === states.default && !deprecationWarned) {
if (unhandledRejectionsMode === kDefaultUnhandledRejections &&
!deprecationWarned) {
deprecationWarned = true;
process.emitWarning(
'Unhandled promise rejections are deprecated. In the future, ' +
Expand Down Expand Up @@ -161,13 +182,27 @@ function processPromiseRejections() {
}
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);
switch (unhandledRejectionsMode) {
case kThrowUnhandledRejections: {
fatalException(reason);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also funny enough there really isn't anything fatal about this function, or triggerFatalException, as they give the user a chance to pretend nothing has happened by listening to uncaughtException.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a legacy name. It does however reflect it's actual purpose. It is just unfortunate that we have an escape hatch for it with process.on('uncaughtException', fn).

const handled = process.emit('unhandledRejection', reason, promise);
if (!handled) emitWarning(uid, reason);
break;
}
case kIgnoreUnhandledRejections: {
process.emit('unhandledRejection', reason, promise);
break;
}
case kAlwaysWarnUnhandledRejections: {
process.emit('unhandledRejection', reason, promise);
emitWarning(uid, reason);
break;
}
case kDefaultUnhandledRejections: {
const handled = process.emit('unhandledRejection', reason, promise);
if (!handled) emitWarning(uid, reason);
break;
}
}
maybeScheduledTicksOrMicrotasks = true;
}
Expand Down