Skip to content

Commit 3814c75

Browse files
committed
process: Throw exception on --unhandled-rejections=default
This is a semver-major change that resolves DEP0018. This PR defines a new mode for the --unhandled-rejections flag, called "default". The "default" mode first emits unhandledRejection. If this hook is not set, the "default" mode will raise the unhandled rejection as an uncaught exception. This mirrors the behavior of the current (unnamed) default mode for --unhandled-rejections, which behaves nearly like "warn" mode. The current default behavior is to emit unhandledRejection; if this hook is not set, the unnamed default mode logs a warning and a deprecation notice. (In comparison, "warn" mode always logs a warning, regardless of whether the hook is set.) 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. Fixes: #20392 Refs: https://nodejs.org/dist/latest/docs/api/deprecations.html#deprecations_dep0018_unhandled_promise_rejections
1 parent 91ca221 commit 3814c75

14 files changed

+34
-85
lines changed

doc/api/cli.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -885,13 +885,11 @@ Track heap object allocations for heap snapshots.
885885
added: v12.0.0
886886
-->
887887

888-
By default all unhandled rejections trigger a warning plus a deprecation warning
889-
for the very first unhandled rejection in case no [`unhandledRejection`][] hook
890-
is used.
891-
892888
Using this flag allows to change what should happen when an unhandled rejection
893889
occurs. One of three modes can be chosen:
894890

891+
* `default`: Emit [`unhandledRejection`][]. If this hook is not set, raise the
892+
unhandled rejection as an uncaught exception. This is the default.
895893
* `strict`: Raise the unhandled rejection as an uncaught exception.
896894
* `warn`: Always trigger a warning, no matter if the [`unhandledRejection`][]
897895
hook is set or not but do not print the deprecation warning.

lib/internal/process/promises.js

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,22 @@ const pendingUnhandledRejections = [];
3030
const asyncHandledRejections = [];
3131
let lastPromiseId = 0;
3232

33-
// --unhandled-rejection=none:
33+
// --unhandled-rejections=none:
3434
// Emit 'unhandledRejection', but do not emit any warning.
3535
const kIgnoreUnhandledRejections = 0;
36-
// --unhandled-rejection=warn:
36+
// --unhandled-rejections=warn:
3737
// Emit 'unhandledRejection', then emit 'UnhandledPromiseRejectionWarning'.
3838
const kAlwaysWarnUnhandledRejections = 1;
39-
// --unhandled-rejection=strict:
39+
// --unhandled-rejections=strict:
4040
// Emit 'uncaughtException'. If it's not handled, print the error to stderr
4141
// and exit the process.
4242
// Otherwise, emit 'unhandledRejection'. If 'unhandledRejection' is not
4343
// handled, emit 'UnhandledPromiseRejectionWarning'.
4444
const kThrowUnhandledRejections = 2;
4545
// --unhandled-rejection is unset:
46-
// Emit 'unhandledRejection', if it's handled, emit
47-
// 'UnhandledPromiseRejectionWarning', then emit deprecation warning.
46+
// Emit 'unhandledRejection', if it's unhandled, emit
47+
// 'uncaughtException'. If it's not handled, print the error to stderr
48+
// and exit the process.
4849
const kDefaultUnhandledRejections = 3;
4950

5051
let unhandledRejectionsMode;
@@ -156,15 +157,6 @@ function emitUnhandledRejectionWarning(uid, reason) {
156157
process.emitWarning(warning);
157158
}
158159

159-
let deprecationWarned = false;
160-
function emitDeprecationWarning() {
161-
process.emitWarning(
162-
'Unhandled promise rejections are deprecated. In the future, ' +
163-
'promise rejections that are not handled will terminate the ' +
164-
'Node.js process with a non-zero exit code.',
165-
'DeprecationWarning', 'DEP0018');
166-
}
167-
168160
// If this method returns true, we've executed user code or triggered
169161
// a warning to be emitted which requires the microtask and next tick
170162
// queues to be drained again.
@@ -208,11 +200,9 @@ function processPromiseRejections() {
208200
case kDefaultUnhandledRejections: {
209201
const handled = process.emit('unhandledRejection', reason, promise);
210202
if (!handled) {
211-
emitUnhandledRejectionWarning(uid, reason);
212-
if (!deprecationWarned) {
213-
emitDeprecationWarning();
214-
deprecationWarned = true;
215-
}
203+
const err = reason instanceof Error ?
204+
reason : generateUnhandledRejectionError(reason);
205+
triggerUncaughtException(err, true /* fromPromise */);
216206
}
217207
break;
218208
}

src/node_options.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) {
119119
}
120120

121121
if (!unhandled_rejections.empty() &&
122+
unhandled_rejections != "default" &&
122123
unhandled_rejections != "strict" &&
123124
unhandled_rejections != "warn" &&
124125
unhandled_rejections != "none") {

test/common/index.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -596,11 +596,8 @@ function getBufferSources(buf) {
596596
return [...getArrayBufferViews(buf), new Uint8Array(buf).buffer];
597597
}
598598

599-
// Crash the process on unhandled rejections.
600-
const crashOnUnhandledRejection = (err) => { throw err; };
601-
process.on('unhandledRejection', crashOnUnhandledRejection);
602599
function disableCrashOnUnhandledRejection() {
603-
process.removeListener('unhandledRejection', crashOnUnhandledRejection);
600+
process.on('unhandledRejection', () => {});
604601
}
605602

606603
function getTTYfd() {

test/es-module/test-esm-cjs-load-error-note.mjs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@ pImport2.stderr.on('data', (data) => {
6363
pImport2Stderr += data;
6464
});
6565
pImport2.on('close', mustCall((code) => {
66-
// As this exits normally we expect 0
67-
assert.strictEqual(code, 0);
66+
assert.strictEqual(code, expectedCode);
6867
assert.ok(!pImport2Stderr.includes(expectedNote),
6968
`${expectedNote} must not be included in ${pImport2Stderr}`);
7069
}));
@@ -94,15 +93,15 @@ pImport4.on('close', mustCall((code) => {
9493
`${expectedNote} not found in ${pImport4Stderr}`);
9594
}));
9695

97-
// Must exit with zero and show note
96+
// Must exit non-zero and show note
9897
const pImport5 = spawn(process.execPath, [Import5]);
9998
let pImport5Stderr = '';
10099
pImport5.stderr.setEncoding('utf8');
101100
pImport5.stderr.on('data', (data) => {
102101
pImport5Stderr += data;
103102
});
104103
pImport5.on('close', mustCall((code) => {
105-
assert.strictEqual(code, 0);
104+
assert.strictEqual(code, expectedCode);
106105
assert.ok(!pImport5Stderr.includes(expectedNote),
107106
`${expectedNote} must not be included in ${pImport5Stderr}`);
108107
}));

test/message/promise_always_throw_unhandled.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Flags: --unhandled-rejections=strict
22
'use strict';
33

4-
require('../common');
4+
const common = require('../common');
55

66
// Check that the process will exit on the first unhandled rejection in case the
77
// unhandled rejections mode is set to `'strict'`.
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
// Flags: --trace-warnings
1+
// Flags: --trace-warnings --unhandled-rejections=warn
22
'use strict';
33
const common = require('../common');
4-
common.disableCrashOnUnhandledRejection();
54
const p = Promise.reject(new Error('This was rejected'));
65
setImmediate(() => p.catch(() => {}));

test/message/unhandled_promise_trace_warnings.out

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@
1717
at *
1818
at *
1919
at *
20-
(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.
21-
at *
22-
at *
23-
at *
2420
(node:*) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
2521
at handledRejection (internal/process/promises.js:*)
2622
at promiseRejectHandler (internal/process/promises.js:*)

test/parallel/test-promise-unhandled-warn-no-hook.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33

44
const common = require('../common');
55

6-
common.disableCrashOnUnhandledRejection();
7-
8-
// Verify that ignoring unhandled rejection works fine and that no warning is
9-
// logged.
6+
// Verify that --unhandled-rejections=warn works fine
107

118
new Promise(() => {
129
throw new Error('One');

test/parallel/test-promises-unhandled-proxy-rejections.js

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,6 @@ const common = require('../common');
33

44
common.disableCrashOnUnhandledRejection();
55

6-
const expectedDeprecationWarning = ['Unhandled promise rejections are ' +
7-
'deprecated. In the future, promise ' +
8-
'rejections that are not handled will ' +
9-
'terminate the Node.js process with a ' +
10-
'non-zero exit code.', 'DEP0018'];
11-
const expectedPromiseWarning = ['Unhandled promise rejection. ' +
12-
'This error originated either by throwing ' +
13-
'inside of an async function without a catch ' +
14-
'block, or by rejecting a promise which was ' +
15-
'not handled with .catch(). To terminate the ' +
16-
'node process on unhandled promise rejection, ' +
17-
'use the CLI flag `--unhandled-rejections=strict` (see ' +
18-
'https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). ' +
19-
'(rejection id: 1)'];
20-
216
function throwErr() {
227
throw new Error('Error from proxy');
238
}
@@ -38,10 +23,7 @@ const thorny = new Proxy({}, {
3823
construct: throwErr
3924
});
4025

41-
common.expectWarning({
42-
DeprecationWarning: expectedDeprecationWarning,
43-
UnhandledPromiseRejectionWarning: expectedPromiseWarning,
44-
});
26+
process.on('warning', common.mustNotCall());
4527

4628
// Ensure this doesn't crash
4729
Promise.reject(thorny);

test/parallel/test-promises-unhandled-rejections.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,7 @@ asyncTest('Throwing an error inside a rejectionHandled handler goes to' +
668668
' unhandledException, and does not cause .catch() to throw an ' +
669669
'exception', function(done) {
670670
clean();
671+
common.disableCrashOnUnhandledRejection();
671672
const e = new Error();
672673
const e2 = new Error();
673674
const tearDownException = setupException(function(err) {
@@ -702,19 +703,20 @@ asyncTest('Rejected promise inside unhandledRejection allows nextTick loop' +
702703
});
703704

704705
asyncTest(
705-
'Unhandled promise rejection emits a warning immediately',
706+
'Promise rejection triggers unhandledRejection immediately',
706707
function(done) {
707708
clean();
708-
Promise.reject(0);
709-
const { emitWarning } = process;
710-
process.emitWarning = common.mustCall((...args) => {
709+
process.on('unhandledRejection', common.mustCall((err) => {
711710
if (timer) {
712711
clearTimeout(timer);
713712
timer = null;
714713
done();
715714
}
716-
emitWarning(...args);
717-
}, 2);
715+
}));
716+
try {
717+
Promise.reject(0);
718+
assert(false);
719+
} catch (e) {}
718720

719721
let timer = setTimeout(common.mustNotCall(), 10000);
720722
},

test/parallel/test-promises-unhandled-symbol-rejections.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1+
// Flags: --unhandled-rejections=warn
12
'use strict';
23
const common = require('../common');
34

45
common.disableCrashOnUnhandledRejection();
56

67
const expectedValueWarning = ['Symbol()'];
7-
const expectedDeprecationWarning = ['Unhandled promise rejections are ' +
8-
'deprecated. In the future, promise ' +
9-
'rejections that are not handled will ' +
10-
'terminate the Node.js process with a ' +
11-
'non-zero exit code.', 'DEP0018'];
128
const expectedPromiseWarning = ['Unhandled promise rejection. ' +
139
'This error originated either by throwing ' +
1410
'inside of an async function without a catch ' +
@@ -20,7 +16,6 @@ const expectedPromiseWarning = ['Unhandled promise rejection. ' +
2016
'(rejection id: 1)'];
2117

2218
common.expectWarning({
23-
DeprecationWarning: expectedDeprecationWarning,
2419
UnhandledPromiseRejectionWarning: [
2520
expectedValueWarning,
2621
expectedPromiseWarning

test/parallel/test-promises-warning-on-unhandled-rejection.js

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Flags: --no-warnings
1+
// Flags: --no-warnings --unhandled-rejections=warn
22
'use strict';
33

44
// Test that warnings are emitted when a Promise experiences an uncaught
@@ -7,8 +7,6 @@
77
const common = require('../common');
88
const assert = require('assert');
99

10-
common.disableCrashOnUnhandledRejection();
11-
1210
let b = 0;
1311

1412
process.on('warning', common.mustCall((warning) => {
@@ -27,14 +25,10 @@ process.on('warning', common.mustCall((warning) => {
2725
);
2826
break;
2927
case 2:
30-
// One time deprecation warning, first unhandled rejection
31-
assert.strictEqual(warning.name, 'DeprecationWarning');
32-
break;
33-
case 3:
3428
// Number rejection error displayed. Note it's been stringified
3529
assert.strictEqual(warning.message, '42');
3630
break;
37-
case 4:
31+
case 3:
3832
// Unhandled rejection warning (won't be handled next tick)
3933
assert.strictEqual(warning.name, 'UnhandledPromiseRejectionWarning');
4034
assert(
@@ -43,13 +37,13 @@ process.on('warning', common.mustCall((warning) => {
4337
`but did not. Had "${warning.message}" instead.`
4438
);
4539
break;
46-
case 5:
40+
case 4:
4741
// Rejection handled asynchronously.
4842
assert.strictEqual(warning.name, 'PromiseRejectionHandledWarning');
4943
assert(/Promise rejection was handled asynchronously/
5044
.test(warning.message));
5145
}
52-
}, 6));
46+
}, 5));
5347

5448
const p = Promise.reject('This was rejected'); // Reject with a string
5549
setImmediate(common.mustCall(() => p.catch(() => { })));

test/sequential/test-inspector-async-call-stack-abort.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ const { strictEqual } = require('assert');
99
const eyecatcher = 'nou, houdoe he?';
1010

1111
if (process.argv[2] === 'child') {
12-
common.disableCrashOnUnhandledRejection();
1312
const { Session } = require('inspector');
1413
const { promisify } = require('util');
1514
const { internalBinding } = require('internal/test/binding');
@@ -31,7 +30,7 @@ if (process.argv[2] === 'child') {
3130
const options = { encoding: 'utf8' };
3231
const proc = spawnSync(
3332
process.execPath, ['--expose-internals', __filename, 'child'], options);
34-
strictEqual(proc.status, 0);
33+
strictEqual(proc.status, 1);
3534
strictEqual(proc.signal, null);
3635
strictEqual(proc.stderr.includes(eyecatcher), true);
3736
}

0 commit comments

Comments
 (0)