process: change default --unhandled-rejections=throw

This is a semver-major change that resolves DEP0018.

All users that have set an unhandledRejection hook or set a non-default
value for the --unhandled-rejections flag will see no change in behavior
after this change.

Refs: https://nodejs.org/dist/latest/docs/api/deprecations.html#deprecations_dep0018_unhandled_promise_rejections

PR-URL: https://github.com/nodejs/node/pull/33021
Fixes: https://github.com/nodejs/node/issues/20392
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Mary Marchini <oss@mmarchini.me>
Reviewed-By: Shelley Vohr <codebytere@gmail.com>
Reviewed-By: Michael Dawson <midawson@redhat.com>
Reviewed-By: Anatoli Papirovski <apapirovski@mac.com>
Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com>
This commit is contained in:
Dan Fabulich 2020-04-23 00:17:18 -07:00 committed by Mary Marchini
parent 25d8456563
commit 3b10f7f933
No known key found for this signature in database
GPG Key ID: BE516BA4874DB4D9
15 changed files with 73 additions and 97 deletions

View File

@ -1007,15 +1007,11 @@ added:
- v10.17.0 - v10.17.0
--> -->
By default all unhandled rejections trigger a warning plus a deprecation warning
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 Using this flag allows to change what should happen when an unhandled rejection
occurs. One of the following modes can be chosen: occurs. One of the following modes can be chosen:
* `throw`: Emit [`unhandledRejection`][]. If this hook is not set, raise the * `throw`: Emit [`unhandledRejection`][]. If this hook is not set, raise the
unhandled rejection as an uncaught exception. unhandled rejection as an uncaught exception. This is the default.
* `strict`: 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`][] * `warn`: Always trigger a warning, no matter if the [`unhandledRejection`][]
hook is set or not but do not print the deprecation warning. hook is set or not but do not print the deprecation warning.

View File

@ -57,11 +57,6 @@ const kThrowUnhandledRejections = 3;
const kWarnWithErrorCodeUnhandledRejections = 4; const kWarnWithErrorCodeUnhandledRejections = 4;
// --unhandled-rejections is unset:
// Emit 'unhandledRejection', if it's unhandled, emit
// 'UnhandledPromiseRejectionWarning', then emit deprecation warning.
const kDefaultUnhandledRejections = 5;
let unhandledRejectionsMode; let unhandledRejectionsMode;
function setHasRejectionToWarn(value) { function setHasRejectionToWarn(value) {
@ -86,7 +81,7 @@ function getUnhandledRejectionsMode() {
case 'warn-with-error-code': case 'warn-with-error-code':
return kWarnWithErrorCodeUnhandledRejections; return kWarnWithErrorCodeUnhandledRejections;
default: default:
return kDefaultUnhandledRejections; return kThrowUnhandledRejections;
} }
} }
@ -175,15 +170,6 @@ function emitUnhandledRejectionWarning(uid, reason) {
process.emitWarning(warning); process.emitWarning(warning);
} }
let deprecationWarned = false;
function emitDeprecationWarning() {
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, we've executed user code or triggered // If this method returns true, we've executed user code or triggered
// a warning to be emitted which requires the microtask and next tick // a warning to be emitted which requires the microtask and next tick
// queues to be drained again. // queues to be drained again.
@ -241,17 +227,6 @@ function processPromiseRejections() {
} }
break; break;
} }
case kDefaultUnhandledRejections: {
const handled = process.emit('unhandledRejection', reason, promise);
if (!handled) {
emitUnhandledRejectionWarning(uid, reason);
if (!deprecationWarned) {
emitDeprecationWarning();
deprecationWarned = true;
}
}
break;
}
} }
maybeScheduledTicksOrMicrotasks = true; maybeScheduledTicksOrMicrotasks = true;
} }

View File

@ -599,11 +599,8 @@ function getBufferSources(buf) {
return [...getArrayBufferViews(buf), new Uint8Array(buf).buffer]; return [...getArrayBufferViews(buf), new Uint8Array(buf).buffer];
} }
// Crash the process on unhandled rejections.
const crashOnUnhandledRejection = (err) => { throw err; };
process.on('unhandledRejection', crashOnUnhandledRejection);
function disableCrashOnUnhandledRejection() { function disableCrashOnUnhandledRejection() {
process.removeListener('unhandledRejection', crashOnUnhandledRejection); process.on('unhandledRejection', () => {});
} }
function getTTYfd() { function getTTYfd() {

View File

@ -63,8 +63,7 @@ pImport2.stderr.on('data', (data) => {
pImport2Stderr += data; pImport2Stderr += data;
}); });
pImport2.on('close', mustCall((code) => { pImport2.on('close', mustCall((code) => {
// As this exits normally we expect 0 assert.strictEqual(code, expectedCode);
assert.strictEqual(code, 0);
assert.ok(!pImport2Stderr.includes(expectedNote), assert.ok(!pImport2Stderr.includes(expectedNote),
`${expectedNote} must not be included in ${pImport2Stderr}`); `${expectedNote} must not be included in ${pImport2Stderr}`);
})); }));
@ -94,7 +93,7 @@ pImport4.on('close', mustCall((code) => {
`${expectedNote} not found in ${pImport4Stderr}`); `${expectedNote} not found in ${pImport4Stderr}`);
})); }));
// Must exit with zero and show note // Must exit non-zero and show note
const pImport5 = spawn(process.execPath, [Import5]); const pImport5 = spawn(process.execPath, [Import5]);
let pImport5Stderr = ''; let pImport5Stderr = '';
pImport5.stderr.setEncoding('utf8'); pImport5.stderr.setEncoding('utf8');
@ -102,7 +101,7 @@ pImport5.stderr.on('data', (data) => {
pImport5Stderr += data; pImport5Stderr += data;
}); });
pImport5.on('close', mustCall((code) => { pImport5.on('close', mustCall((code) => {
assert.strictEqual(code, 0); assert.strictEqual(code, expectedCode);
assert.ok(!pImport5Stderr.includes(expectedNote), assert.ok(!pImport5Stderr.includes(expectedNote),
`${expectedNote} must not be included in ${pImport5Stderr}`); `${expectedNote} must not be included in ${pImport5Stderr}`);
})); }));

View File

@ -1,10 +1,8 @@
// Flags: --unhandled-rejections=warn-with-error-code // Flags: --unhandled-rejections=warn-with-error-code
'use strict'; 'use strict';
const common = require('../common'); require('../common');
const assert = require('assert'); const assert = require('assert');
common.disableCrashOnUnhandledRejection();
Promise.reject(new Error('alas')); Promise.reject(new Error('alas'));
process.on('exit', assert.strictEqual.bind(null, 1)); process.on('exit', assert.strictEqual.bind(null, 1));

View File

@ -1,6 +1,5 @@
// Flags: --trace-warnings // Flags: --trace-warnings --unhandled-rejections=warn
'use strict'; 'use strict';
const common = require('../common'); require('../common');
common.disableCrashOnUnhandledRejection();
const p = Promise.reject(new Error('This was rejected')); const p = Promise.reject(new Error('This was rejected'));
setImmediate(() => p.catch(() => {})); setImmediate(() => p.catch(() => {}));

View File

@ -17,10 +17,6 @@
at * at *
at * at *
at * at *
(node:*) [DEP0018] DeprecationWarning: 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.
at *
at *
at *
(node:*) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1) (node:*) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
at handledRejection (internal/process/promises.js:*) at handledRejection (internal/process/promises.js:*)
at promiseRejectHandler (internal/process/promises.js:*) at promiseRejectHandler (internal/process/promises.js:*)

View File

@ -0,0 +1,52 @@
'use strict';
const common = require('../common');
const Countdown = require('../common/countdown');
const assert = require('assert');
// 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));

View File

@ -5,8 +5,6 @@ const common = require('../common');
const Countdown = require('../common/countdown'); const Countdown = require('../common/countdown');
const assert = require('assert'); const assert = require('assert');
common.disableCrashOnUnhandledRejection();
// Verify that unhandled rejections always trigger uncaught exceptions instead // Verify that unhandled rejections always trigger uncaught exceptions instead
// of triggering unhandled rejections. // of triggering unhandled rejections.

View File

@ -3,10 +3,7 @@
const common = require('../common'); const common = require('../common');
common.disableCrashOnUnhandledRejection(); // Verify that --unhandled-rejections=warn works fine
// Verify that ignoring unhandled rejection works fine and that no warning is
// logged.
new Promise(() => { new Promise(() => {
throw new Error('One'); throw new Error('One');

View File

@ -3,21 +3,6 @@ const common = require('../common');
common.disableCrashOnUnhandledRejection(); common.disableCrashOnUnhandledRejection();
const expectedDeprecationWarning = ['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.', 'DEP0018'];
const expectedPromiseWarning = ['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)'];
function throwErr() { function throwErr() {
throw new Error('Error from proxy'); throw new Error('Error from proxy');
} }
@ -38,10 +23,7 @@ const thorny = new Proxy({}, {
construct: throwErr construct: throwErr
}); });
common.expectWarning({ process.on('warning', common.mustNotCall());
DeprecationWarning: expectedDeprecationWarning,
UnhandledPromiseRejectionWarning: expectedPromiseWarning,
});
// Ensure this doesn't crash // Ensure this doesn't crash
Promise.reject(thorny); Promise.reject(thorny);

View File

@ -668,6 +668,7 @@ asyncTest('Throwing an error inside a rejectionHandled handler goes to' +
' unhandledException, and does not cause .catch() to throw an ' + ' unhandledException, and does not cause .catch() to throw an ' +
'exception', function(done) { 'exception', function(done) {
clean(); clean();
common.disableCrashOnUnhandledRejection();
const e = new Error(); const e = new Error();
const e2 = new Error(); const e2 = new Error();
const tearDownException = setupException(function(err) { const tearDownException = setupException(function(err) {
@ -702,19 +703,17 @@ asyncTest('Rejected promise inside unhandledRejection allows nextTick loop' +
}); });
asyncTest( asyncTest(
'Unhandled promise rejection emits a warning immediately', 'Promise rejection triggers unhandledRejection immediately',
function(done) { function(done) {
clean(); clean();
Promise.reject(0); Promise.reject(0);
const { emitWarning } = process; process.on('unhandledRejection', common.mustCall((err) => {
process.emitWarning = common.mustCall((...args) => {
if (timer) { if (timer) {
clearTimeout(timer); clearTimeout(timer);
timer = null; timer = null;
done(); done();
} }
emitWarning(...args); }));
}, 2);
let timer = setTimeout(common.mustNotCall(), 10000); let timer = setTimeout(common.mustNotCall(), 10000);
}, },

View File

@ -1,14 +1,10 @@
// Flags: --unhandled-rejections=warn
'use strict'; 'use strict';
const common = require('../common'); const common = require('../common');
common.disableCrashOnUnhandledRejection(); common.disableCrashOnUnhandledRejection();
const expectedValueWarning = ['Symbol()']; const expectedValueWarning = ['Symbol()'];
const expectedDeprecationWarning = ['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.', 'DEP0018'];
const expectedPromiseWarning = ['Unhandled promise rejection. ' + const expectedPromiseWarning = ['Unhandled promise rejection. ' +
'This error originated either by throwing ' + 'This error originated either by throwing ' +
'inside of an async function without a catch ' + 'inside of an async function without a catch ' +
@ -20,7 +16,6 @@ const expectedPromiseWarning = ['Unhandled promise rejection. ' +
'(rejection id: 1)']; '(rejection id: 1)'];
common.expectWarning({ common.expectWarning({
DeprecationWarning: expectedDeprecationWarning,
UnhandledPromiseRejectionWarning: [ UnhandledPromiseRejectionWarning: [
expectedValueWarning, expectedValueWarning,
expectedPromiseWarning expectedPromiseWarning

View File

@ -1,4 +1,4 @@
// Flags: --no-warnings // Flags: --no-warnings --unhandled-rejections=warn
'use strict'; 'use strict';
// Test that warnings are emitted when a Promise experiences an uncaught // Test that warnings are emitted when a Promise experiences an uncaught
@ -7,8 +7,6 @@
const common = require('../common'); const common = require('../common');
const assert = require('assert'); const assert = require('assert');
common.disableCrashOnUnhandledRejection();
let b = 0; let b = 0;
process.on('warning', common.mustCall((warning) => { process.on('warning', common.mustCall((warning) => {
@ -27,14 +25,10 @@ process.on('warning', common.mustCall((warning) => {
); );
break; break;
case 2: case 2:
// One time deprecation warning, first unhandled rejection
assert.strictEqual(warning.name, 'DeprecationWarning');
break;
case 3:
// Number rejection error displayed. Note it's been stringified // Number rejection error displayed. Note it's been stringified
assert.strictEqual(warning.message, '42'); assert.strictEqual(warning.message, '42');
break; break;
case 4: case 3:
// Unhandled rejection warning (won't be handled next tick) // Unhandled rejection warning (won't be handled next tick)
assert.strictEqual(warning.name, 'UnhandledPromiseRejectionWarning'); assert.strictEqual(warning.name, 'UnhandledPromiseRejectionWarning');
assert( assert(
@ -43,13 +37,13 @@ process.on('warning', common.mustCall((warning) => {
`but did not. Had "${warning.message}" instead.` `but did not. Had "${warning.message}" instead.`
); );
break; break;
case 5: case 4:
// Rejection handled asynchronously. // Rejection handled asynchronously.
assert.strictEqual(warning.name, 'PromiseRejectionHandledWarning'); assert.strictEqual(warning.name, 'PromiseRejectionHandledWarning');
assert(/Promise rejection was handled asynchronously/ assert(/Promise rejection was handled asynchronously/
.test(warning.message)); .test(warning.message));
} }
}, 6)); }, 5));
const p = Promise.reject('This was rejected'); // Reject with a string const p = Promise.reject('This was rejected'); // Reject with a string
setImmediate(common.mustCall(() => p.catch(() => { }))); setImmediate(common.mustCall(() => p.catch(() => { })));

View File

@ -9,7 +9,6 @@ const { strictEqual } = require('assert');
const eyecatcher = 'nou, houdoe he?'; const eyecatcher = 'nou, houdoe he?';
if (process.argv[2] === 'child') { if (process.argv[2] === 'child') {
common.disableCrashOnUnhandledRejection();
const { Session } = require('inspector'); const { Session } = require('inspector');
const { promisify } = require('util'); const { promisify } = require('util');
const { internalBinding } = require('internal/test/binding'); const { internalBinding } = require('internal/test/binding');
@ -31,7 +30,7 @@ if (process.argv[2] === 'child') {
const options = { encoding: 'utf8' }; const options = { encoding: 'utf8' };
const proc = spawnSync( const proc = spawnSync(
process.execPath, ['--expose-internals', __filename, 'child'], options); process.execPath, ['--expose-internals', __filename, 'child'], options);
strictEqual(proc.status, 0); strictEqual(proc.status, 1);
strictEqual(proc.signal, null); strictEqual(proc.signal, null);
strictEqual(proc.stderr.includes(eyecatcher), true); strictEqual(proc.stderr.includes(eyecatcher), true);
} }