-
Notifications
You must be signed in to change notification settings - Fork 30.1k
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: add --unhandled-rejections flag #26599
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,8 @@ | ||
'use strict'; | ||
|
||
const { safeToString } = internalBinding('util'); | ||
const { | ||
safeToString | ||
} = internalBinding('util'); | ||
const { | ||
tickInfo, | ||
promiseRejectEvents: { | ||
|
@@ -9,7 +11,8 @@ const { | |
kPromiseResolveAfterResolved, | ||
kPromiseRejectAfterResolved | ||
}, | ||
setPromiseRejectCallback | ||
setPromiseRejectCallback, | ||
triggerFatalException | ||
} = internalBinding('task_queue'); | ||
|
||
// *Must* match Environment::TickInfo::Fields in src/env.h. | ||
|
@@ -20,6 +23,15 @@ const pendingUnhandledRejections = []; | |
const asyncHandledRejections = []; | ||
let lastPromiseId = 0; | ||
|
||
const states = { | ||
none: 0, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. minor non-blocking nit... I'd prefer if these were defined as constants const kStateNone = 0;
const kStateWarn = 1;
const kStateStrict = 2;
const kStateDefault = 3; And would prefer a simple switch rather than an object lookup. For super small sets like this the switch should be faster There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll open a follow-up PR for the two nits. I would like to land this as is, otherwise other's have to look at this again. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The switch case is only executed once, so it's not about performance. |
||
warn: 1, | ||
strict: 2, | ||
default: 3 | ||
}; | ||
|
||
let state; | ||
ChALkeR marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
function setHasRejectionToWarn(value) { | ||
tickInfo[kHasRejectionToWarn] = value ? 1 : 0; | ||
} | ||
|
@@ -29,6 +41,10 @@ function hasRejectionToWarn() { | |
} | ||
|
||
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); | ||
|
@@ -59,6 +75,7 @@ function unhandledRejection(promise, reason) { | |
uid: ++lastPromiseId, | ||
warned: false | ||
}); | ||
// This causes the promise to be referenced at least for one tick. | ||
pendingUnhandledRejections.push(promise); | ||
setHasRejectionToWarn(true); | ||
} | ||
|
@@ -85,14 +102,16 @@ function handledRejection(promise) { | |
|
||
const unhandledRejectionErrName = 'UnhandledPromiseRejectionWarning'; | ||
function emitWarning(uid, reason) { | ||
// eslint-disable-next-line no-restricted-syntax | ||
const warning = new Error( | ||
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})` | ||
'throwing inside of an async function without a catch block, ' + | ||
'or by rejecting a promise which was not handled with .catch(). ' + | ||
`(rejection id: ${uid})` | ||
); | ||
warning.name = unhandledRejectionErrName; | ||
try { | ||
if (reason instanceof Error) { | ||
warning.stack = reason.stack; | ||
|
@@ -108,7 +127,7 @@ function emitWarning(uid, reason) { | |
|
||
let deprecationWarned = false; | ||
function emitDeprecationWarning() { | ||
if (!deprecationWarned) { | ||
if (state === states.default && !deprecationWarned) { | ||
deprecationWarned = true; | ||
process.emitWarning( | ||
'Unhandled promise rejections are deprecated. In the future, ' + | ||
|
@@ -133,18 +152,57 @@ function processPromiseRejections() { | |
while (len--) { | ||
const promise = pendingUnhandledRejections.shift(); | ||
const promiseInfo = maybeUnhandledPromises.get(promise); | ||
if (promiseInfo !== undefined) { | ||
promiseInfo.warned = true; | ||
const { reason, uid } = promiseInfo; | ||
if (!process.emit('unhandledRejection', reason, promise)) { | ||
emitWarning(uid, reason); | ||
} | ||
maybeScheduledTicks = true; | ||
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); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be
error
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally planned on using
error
andsilence
instead ofstrict
andnone
but others preferred these and I have no strong opinion on the name in this case.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: may want to add a quick statement that the
mode
is case-sensitive.