Skip to content

Commit f66bb57

Browse files
dfabulichcodebytere
authored andcommitted
process: add unhandled-rejection throw and warn-with-error-code
This PR defines two new modes for the --unhandled-rejections flag. The first mode is called "throw". The "throw" mode first emits unhandledRejection. If this hook is not set, the "throw" mode will raise the unhandled rejection as an uncaught exception. The second mode is called "warn-with-error-code". The "warn-with-error-code" mode first emits unhandledRejection. If this hook is not set, the "warn-with-error-code" mode will trigger a warning and set the process's exit code to 1. The PR doesn't change the default behavior for unhandled rejections. That will come in a separate PR. Refs: #33021 PR-URL: #33475 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Anna Henningsen <[email protected]>
1 parent f1b0291 commit f66bb57

7 files changed

+164
-9
lines changed

doc/api/cli.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -952,11 +952,15 @@ for the very first unhandled rejection in case no [`unhandledRejection`][] hook
952952
is used.
953953

954954
Using this flag allows to change what should happen when an unhandled rejection
955-
occurs. One of three modes can be chosen:
955+
occurs. One of the following modes can be chosen:
956956

957+
* `throw`: Emit [`unhandledRejection`][]. If this hook is not set, raise the
958+
unhandled rejection as an uncaught exception.
957959
* `strict`: Raise the unhandled rejection as an uncaught exception.
958960
* `warn`: Always trigger a warning, no matter if the [`unhandledRejection`][]
959961
hook is set or not but do not print the deprecation warning.
962+
* `warn-with-error-code`: Emit [`unhandledRejection`][]. If this hook is not
963+
set, trigger a warning, and set the process exit code to 1.
960964
* `none`: Silence all warnings.
961965

962966
### `--use-bundled-ca`, `--use-openssl-ca`

lib/internal/process/promises.js

+44-8
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,37 @@ 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+
37+
// --unhandled-rejections=warn:
3738
// Emit 'unhandledRejection', then emit 'UnhandledPromiseRejectionWarning'.
3839
const kAlwaysWarnUnhandledRejections = 1;
39-
// --unhandled-rejection=strict:
40+
41+
// --unhandled-rejections=strict:
4042
// Emit 'uncaughtException'. If it's not handled, print the error to stderr
4143
// and exit the process.
4244
// Otherwise, emit 'unhandledRejection'. If 'unhandledRejection' is not
4345
// handled, emit 'UnhandledPromiseRejectionWarning'.
44-
const kThrowUnhandledRejections = 2;
45-
// --unhandled-rejection is unset:
46-
// Emit 'unhandledRejection', if it's handled, emit
46+
const kStrictUnhandledRejections = 2;
47+
48+
// --unhandled-rejections=throw:
49+
// Emit 'unhandledRejection', if it's unhandled, emit
50+
// 'uncaughtException'. If it's not handled, print the error to stderr
51+
// and exit the process.
52+
const kThrowUnhandledRejections = 3;
53+
54+
// --unhandled-rejections=warn-with-error-code:
55+
// Emit 'unhandledRejection', if it's unhandled, emit
56+
// 'UnhandledPromiseRejectionWarning', then set process exit code to 1.
57+
58+
const kWarnWithErrorCodeUnhandledRejections = 4;
59+
60+
// --unhandled-rejections is unset:
61+
// Emit 'unhandledRejection', if it's unhandled, emit
4762
// 'UnhandledPromiseRejectionWarning', then emit deprecation warning.
48-
const kDefaultUnhandledRejections = 3;
63+
const kDefaultUnhandledRejections = 5;
4964

5065
let unhandledRejectionsMode;
5166

@@ -65,7 +80,11 @@ function getUnhandledRejectionsMode() {
6580
case 'warn':
6681
return kAlwaysWarnUnhandledRejections;
6782
case 'strict':
83+
return kStrictUnhandledRejections;
84+
case 'throw':
6885
return kThrowUnhandledRejections;
86+
case 'warn-with-error-code':
87+
return kWarnWithErrorCodeUnhandledRejections;
6988
default:
7089
return kDefaultUnhandledRejections;
7190
}
@@ -188,7 +207,7 @@ function processPromiseRejections() {
188207
promiseInfo.warned = true;
189208
const { reason, uid } = promiseInfo;
190209
switch (unhandledRejectionsMode) {
191-
case kThrowUnhandledRejections: {
210+
case kStrictUnhandledRejections: {
192211
const err = reason instanceof Error ?
193212
reason : generateUnhandledRejectionError(reason);
194213
triggerUncaughtException(err, true /* fromPromise */);
@@ -205,6 +224,23 @@ function processPromiseRejections() {
205224
emitUnhandledRejectionWarning(uid, reason);
206225
break;
207226
}
227+
case kThrowUnhandledRejections: {
228+
const handled = process.emit('unhandledRejection', reason, promise);
229+
if (!handled) {
230+
const err = reason instanceof Error ?
231+
reason : generateUnhandledRejectionError(reason);
232+
triggerUncaughtException(err, true /* fromPromise */);
233+
}
234+
break;
235+
}
236+
case kWarnWithErrorCodeUnhandledRejections: {
237+
const handled = process.emit('unhandledRejection', reason, promise);
238+
if (!handled) {
239+
emitUnhandledRejectionWarning(uid, reason);
240+
process.exitCode = 1;
241+
}
242+
break;
243+
}
208244
case kDefaultUnhandledRejections: {
209245
const handled = process.emit('unhandledRejection', reason, promise);
210246
if (!handled) {

src/node_options.cc

+2
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) {
118118
}
119119

120120
if (!unhandled_rejections.empty() &&
121+
unhandled_rejections != "warn-with-error-code" &&
122+
unhandled_rejections != "throw" &&
121123
unhandled_rejections != "strict" &&
122124
unhandled_rejections != "warn" &&
123125
unhandled_rejections != "none") {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Flags: --unhandled-rejections=warn-with-error-code
2+
'use strict';
3+
4+
const common = require('../common');
5+
const assert = require('assert');
6+
7+
common.disableCrashOnUnhandledRejection();
8+
9+
Promise.reject(new Error('alas'));
10+
process.on('exit', assert.strictEqual.bind(null, 1));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
*UnhandledPromiseRejectionWarning: Error: alas
2+
at *promise_unhandled_warn_with_error.js:*:*
3+
at *
4+
at *
5+
at *
6+
at *
7+
at *
8+
at *
9+
(Use `node --trace-warnings ...` to show where the warning was created)
10+
*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)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Flags: --unhandled-rejections=throw
2+
'use strict';
3+
4+
const common = require('../common');
5+
const Countdown = require('../common/countdown');
6+
const assert = require('assert');
7+
8+
common.disableCrashOnUnhandledRejection();
9+
10+
// Verify that the unhandledRejection handler prevents triggering
11+
// uncaught exceptions
12+
13+
const err1 = new Error('One');
14+
15+
const errors = [err1, null];
16+
17+
const ref = new Promise(() => {
18+
throw err1;
19+
});
20+
// Explicitly reject `null`.
21+
Promise.reject(null);
22+
23+
process.on('warning', common.mustNotCall('warning'));
24+
process.on('rejectionHandled', common.mustNotCall('rejectionHandled'));
25+
process.on('exit', assert.strictEqual.bind(null, 0));
26+
process.on('uncaughtException', common.mustNotCall('uncaughtException'));
27+
28+
const timer = setTimeout(() => console.log(ref), 1000);
29+
30+
const counter = new Countdown(2, () => {
31+
clearTimeout(timer);
32+
});
33+
34+
process.on('unhandledRejection', common.mustCall((err) => {
35+
counter.dec();
36+
const knownError = errors.shift();
37+
assert.deepStrictEqual(err, knownError);
38+
}, 2));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Flags: --unhandled-rejections=throw
2+
'use strict';
3+
4+
const common = require('../common');
5+
const Countdown = require('../common/countdown');
6+
const assert = require('assert');
7+
8+
common.disableCrashOnUnhandledRejection();
9+
10+
// Verify that unhandled rejections always trigger uncaught exceptions instead
11+
// of triggering unhandled rejections.
12+
13+
const err1 = new Error('One');
14+
const err2 = new Error(
15+
'This error originated either by throwing ' +
16+
'inside of an async function without a catch block, or by rejecting a ' +
17+
'promise which was not handled with .catch(). The promise rejected with the' +
18+
' reason "null".'
19+
);
20+
err2.code = 'ERR_UNHANDLED_REJECTION';
21+
Object.defineProperty(err2, 'name', {
22+
value: 'UnhandledPromiseRejection',
23+
writable: true,
24+
configurable: true
25+
});
26+
27+
const errors = [err1, err2];
28+
const identical = [true, false];
29+
30+
const ref = new Promise(() => {
31+
throw err1;
32+
});
33+
// Explicitly reject `null`.
34+
Promise.reject(null);
35+
36+
process.on('warning', common.mustNotCall('warning'));
37+
// If we add an unhandledRejection handler, the exception won't be thrown
38+
// process.on('unhandledRejection', common.mustCall(2));
39+
process.on('rejectionHandled', common.mustNotCall('rejectionHandled'));
40+
process.on('exit', assert.strictEqual.bind(null, 0));
41+
42+
const timer = setTimeout(() => console.log(ref), 1000);
43+
44+
const counter = new Countdown(2, () => {
45+
clearTimeout(timer);
46+
});
47+
48+
process.on('uncaughtException', common.mustCall((err, origin) => {
49+
counter.dec();
50+
assert.strictEqual(origin, 'unhandledRejection', err);
51+
const knownError = errors.shift();
52+
assert.deepStrictEqual(err, knownError);
53+
// Check if the errors are reference equal.
54+
assert(identical.shift() ? err === knownError : err !== knownError);
55+
}, 2));

0 commit comments

Comments
 (0)