diff --git a/doc/api/cli.md b/doc/api/cli.md index d31fa43fb6a6e7..cee1a9622bf2ce 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -952,11 +952,15 @@ for the very first unhandled rejection in case no [`unhandledRejection`][] hook is used. Using this flag allows to change what should happen when an unhandled rejection -occurs. One of three modes can be chosen: +occurs. One of the following modes can be chosen: +* `throw`: Emit [`unhandledRejection`][]. If this hook is not set, raise the + unhandled rejection as an uncaught exception. * `strict`: Raise the unhandled rejection as an uncaught exception. * `warn`: Always trigger a warning, no matter if the [`unhandledRejection`][] hook is set or not but do not print the deprecation warning. +* `warn-with-error-code`: Emit [`unhandledRejection`][]. If this hook is not + set, trigger a warning, and set the process exit code to 1. * `none`: Silence all warnings. ### `--use-bundled-ca`, `--use-openssl-ca` diff --git a/lib/internal/process/promises.js b/lib/internal/process/promises.js index 1a0ed06f213df8..f2145d425c620f 100644 --- a/lib/internal/process/promises.js +++ b/lib/internal/process/promises.js @@ -30,22 +30,37 @@ const pendingUnhandledRejections = []; const asyncHandledRejections = []; let lastPromiseId = 0; -// --unhandled-rejection=none: +// --unhandled-rejections=none: // Emit 'unhandledRejection', but do not emit any warning. const kIgnoreUnhandledRejections = 0; -// --unhandled-rejection=warn: + +// --unhandled-rejections=warn: // Emit 'unhandledRejection', then emit 'UnhandledPromiseRejectionWarning'. const kAlwaysWarnUnhandledRejections = 1; -// --unhandled-rejection=strict: + +// --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 kThrowUnhandledRejections = 2; -// --unhandled-rejection is unset: -// Emit 'unhandledRejection', if it's handled, emit +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; + +// --unhandled-rejections is unset: +// Emit 'unhandledRejection', if it's unhandled, emit // 'UnhandledPromiseRejectionWarning', then emit deprecation warning. -const kDefaultUnhandledRejections = 3; +const kDefaultUnhandledRejections = 5; let unhandledRejectionsMode; @@ -65,7 +80,11 @@ function getUnhandledRejectionsMode() { case 'warn': return kAlwaysWarnUnhandledRejections; case 'strict': + return kStrictUnhandledRejections; + case 'throw': return kThrowUnhandledRejections; + case 'warn-with-error-code': + return kWarnWithErrorCodeUnhandledRejections; default: return kDefaultUnhandledRejections; } @@ -188,7 +207,7 @@ function processPromiseRejections() { promiseInfo.warned = true; const { reason, uid } = promiseInfo; switch (unhandledRejectionsMode) { - case kThrowUnhandledRejections: { + case kStrictUnhandledRejections: { const err = reason instanceof Error ? reason : generateUnhandledRejectionError(reason); triggerUncaughtException(err, true /* fromPromise */); @@ -205,6 +224,23 @@ function processPromiseRejections() { emitUnhandledRejectionWarning(uid, reason); break; } + case kThrowUnhandledRejections: { + const handled = process.emit('unhandledRejection', reason, promise); + if (!handled) { + const err = reason instanceof Error ? + reason : generateUnhandledRejectionError(reason); + triggerUncaughtException(err, true /* fromPromise */); + } + break; + } + case kWarnWithErrorCodeUnhandledRejections: { + const handled = process.emit('unhandledRejection', reason, promise); + if (!handled) { + emitUnhandledRejectionWarning(uid, reason); + process.exitCode = 1; + } + break; + } case kDefaultUnhandledRejections: { const handled = process.emit('unhandledRejection', reason, promise); if (!handled) { diff --git a/src/node_options.cc b/src/node_options.cc index f4d6e75acd9d07..309a7bd505a1e8 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -118,6 +118,8 @@ void EnvironmentOptions::CheckOptions(std::vector* errors) { } if (!unhandled_rejections.empty() && + unhandled_rejections != "warn-with-error-code" && + unhandled_rejections != "throw" && unhandled_rejections != "strict" && unhandled_rejections != "warn" && unhandled_rejections != "none") { diff --git a/test/message/promise_unhandled_warn_with_error.js b/test/message/promise_unhandled_warn_with_error.js new file mode 100644 index 00000000000000..0f22e8deeba653 --- /dev/null +++ b/test/message/promise_unhandled_warn_with_error.js @@ -0,0 +1,10 @@ +// Flags: --unhandled-rejections=warn-with-error-code +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +common.disableCrashOnUnhandledRejection(); + +Promise.reject(new Error('alas')); +process.on('exit', assert.strictEqual.bind(null, 1)); diff --git a/test/message/promise_unhandled_warn_with_error.out b/test/message/promise_unhandled_warn_with_error.out new file mode 100644 index 00000000000000..b539adb2d1e769 --- /dev/null +++ b/test/message/promise_unhandled_warn_with_error.out @@ -0,0 +1,10 @@ +*UnhandledPromiseRejectionWarning: Error: alas + at *promise_unhandled_warn_with_error.js:*:* + at * + at * + at * + at * + at * + at * +(Use `node --trace-warnings ...` to show where the warning was created) +*UnhandledPromiseRejectionWarning: 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: 1) \ No newline at end of file diff --git a/test/parallel/test-promise-unhandled-throw-handler.js b/test/parallel/test-promise-unhandled-throw-handler.js new file mode 100644 index 00000000000000..be441c2d34c6bd --- /dev/null +++ b/test/parallel/test-promise-unhandled-throw-handler.js @@ -0,0 +1,38 @@ +// Flags: --unhandled-rejections=throw +'use strict'; + +const common = require('../common'); +const Countdown = require('../common/countdown'); +const assert = require('assert'); + +common.disableCrashOnUnhandledRejection(); + +// Verify that the unhandledRejection handler prevents triggering +// uncaught exceptions + +const err1 = new Error('One'); + +const errors = [err1, null]; + +const ref = new Promise(() => { + throw err1; +}); +// Explicitly reject `null`. +Promise.reject(null); + +process.on('warning', common.mustNotCall('warning')); +process.on('rejectionHandled', common.mustNotCall('rejectionHandled')); +process.on('exit', assert.strictEqual.bind(null, 0)); +process.on('uncaughtException', common.mustNotCall('uncaughtException')); + +const timer = setTimeout(() => console.log(ref), 1000); + +const counter = new Countdown(2, () => { + clearTimeout(timer); +}); + +process.on('unhandledRejection', common.mustCall((err) => { + counter.dec(); + const knownError = errors.shift(); + assert.deepStrictEqual(err, knownError); +}, 2)); diff --git a/test/parallel/test-promise-unhandled-throw.js b/test/parallel/test-promise-unhandled-throw.js new file mode 100644 index 00000000000000..30c6b87135b49b --- /dev/null +++ b/test/parallel/test-promise-unhandled-throw.js @@ -0,0 +1,55 @@ +// Flags: --unhandled-rejections=throw +'use strict'; + +const common = require('../common'); +const Countdown = require('../common/countdown'); +const assert = require('assert'); + +common.disableCrashOnUnhandledRejection(); + +// Verify that unhandled rejections always trigger uncaught exceptions instead +// of triggering unhandled rejections. + +const err1 = new Error('One'); +const err2 = new Error( + '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 "null".' +); +err2.code = 'ERR_UNHANDLED_REJECTION'; +Object.defineProperty(err2, 'name', { + value: 'UnhandledPromiseRejection', + writable: true, + configurable: true +}); + +const errors = [err1, err2]; +const identical = [true, false]; + +const ref = new Promise(() => { + throw err1; +}); +// Explicitly reject `null`. +Promise.reject(null); + +process.on('warning', common.mustNotCall('warning')); +// If we add an unhandledRejection handler, the exception won't be thrown +// process.on('unhandledRejection', common.mustCall(2)); +process.on('rejectionHandled', common.mustNotCall('rejectionHandled')); +process.on('exit', assert.strictEqual.bind(null, 0)); + +const timer = setTimeout(() => console.log(ref), 1000); + +const counter = new Countdown(2, () => { + clearTimeout(timer); +}); + +process.on('uncaughtException', common.mustCall((err, origin) => { + counter.dec(); + assert.strictEqual(origin, 'unhandledRejection', err); + const knownError = errors.shift(); + assert.deepStrictEqual(err, knownError); + // Check if the errors are reference equal. + assert(identical.shift() ? err === knownError : err !== knownError); +}, 2));