Skip to content

Commit b43d7e8

Browse files
BridgeARBethGriggs
authored andcommitted
process: add --unhandled-rejections flag
This adds a flag to define the default behavior for unhandled rejections. Three modes exist: `none`, `warn` and `strict`. The first is going to silence all unhandled rejection warnings. The second behaves identical to the current default with the excetion that no deprecation warning will be printed and the last is going to throw an error for each unhandled rejection, just as regular exceptions do. It is possible to intercept those with the `uncaughtException` hook as with all other exceptions as well. This PR has no influence on the existing `unhandledRejection` hook. If that is used, it will continue to function as before. PR-URL: #26599 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Matheus Marchini <[email protected]> Reviewed-By: Michael Dawson <[email protected]> Reviewed-By: Сковорода Никита Андреевич <[email protected]> Reviewed-By: Sakthipriyan Vairamani <[email protected]>
1 parent c285e69 commit b43d7e8

23 files changed

+365
-47
lines changed

doc/api/cli.md

+19
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,23 @@ added: v2.4.0
401401

402402
Track heap object allocations for heap snapshots.
403403

404+
### `--unhandled-rejections=mode`
405+
<!-- YAML
406+
added: REPLACEME
407+
-->
408+
409+
By default all unhandled rejections trigger a warning plus a deprecation warning
410+
for the very first unhandled rejection in case no [`unhandledRejection`][] hook
411+
is used.
412+
413+
Using this flag allows to change what should happen when an unhandled rejection
414+
occurs. One of three modes can be chosen:
415+
416+
* `strict`: Raise the unhandled rejection as an uncaught exception.
417+
* `warn`: Always trigger a warning, no matter if the [`unhandledRejection`][]
418+
hook is set or not but do not print the deprecation warning.
419+
* `none`: Silence all warnings.
420+
404421
### `--use-bundled-ca`, `--use-openssl-ca`
405422
<!-- YAML
406423
added: v6.11.0
@@ -614,6 +631,7 @@ Node.js options that are allowed are:
614631
- `--trace-sync-io`
615632
- `--trace-warnings`
616633
- `--track-heap-objects`
634+
- `--unhandled-rejections`
617635
- `--use-bundled-ca`
618636
- `--use-openssl-ca`
619637
- `--v8-pool-size`
@@ -773,6 +791,7 @@ greater than `4` (its current default value). For more information, see the
773791
[`Buffer`]: buffer.html#buffer_class_buffer
774792
[`SlowBuffer`]: buffer.html#buffer_class_slowbuffer
775793
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
794+
[`unhandledRejection`]: process.html#process_event_unhandledrejection
776795
[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/
777796
[REPL]: repl.html
778797
[ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage

doc/api/process.md

+23-16
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,17 @@ most convenient for scripts).
200200
### Event: 'uncaughtException'
201201
<!-- YAML
202202
added: v0.1.18
203+
changes:
204+
- version: REPLACEME
205+
pr-url: https://github.com/nodejs/node/pull/26599
206+
description: Added the `origin` argument.
203207
-->
204208

209+
* `err` {Error} The uncaught exception.
210+
* `origin` {string} Indicates if the exception originates from an unhandled
211+
rejection or from synchronous errors. Can either be `'uncaughtException'` or
212+
`'unhandledRejection'`.
213+
205214
The `'uncaughtException'` event is emitted when an uncaught JavaScript
206215
exception bubbles all the way back to the event loop. By default, Node.js
207216
handles such exceptions by printing the stack trace to `stderr` and exiting
@@ -212,12 +221,13 @@ behavior. Alternatively, change the [`process.exitCode`][] in the
212221
provided exit code. Otherwise, in the presence of such handler the process will
213222
exit with 0.
214223

215-
The listener function is called with the `Error` object passed as the only
216-
argument.
217-
218224
```js
219-
process.on('uncaughtException', (err) => {
220-
fs.writeSync(1, `Caught exception: ${err}\n`);
225+
process.on('uncaughtException', (err, origin) => {
226+
fs.writeSync(
227+
process.stderr.fd,
228+
`Caught exception: ${err}\n` +
229+
`Exception origin: ${origin}`
230+
);
221231
});
222232

223233
setTimeout(() => {
@@ -269,6 +279,10 @@ changes:
269279
a process warning.
270280
-->
271281

282+
* `reason` {Error|any} The object with which the promise was rejected
283+
(typically an [`Error`][] object).
284+
* `promise` {Promise} The rejected promise.
285+
272286
The `'unhandledRejection'` event is emitted whenever a `Promise` is rejected and
273287
no error handler is attached to the promise within a turn of the event loop.
274288
When programming with Promises, exceptions are encapsulated as "rejected
@@ -277,16 +291,10 @@ are propagated through a `Promise` chain. The `'unhandledRejection'` event is
277291
useful for detecting and keeping track of promises that were rejected whose
278292
rejections have not yet been handled.
279293

280-
The listener function is called with the following arguments:
281-
282-
* `reason` {Error|any} The object with which the promise was rejected
283-
(typically an [`Error`][] object).
284-
* `p` the `Promise` that was rejected.
285-
286294
```js
287-
process.on('unhandledRejection', (reason, p) => {
288-
console.log('Unhandled Rejection at:', p, 'reason:', reason);
289-
// application specific logging, throwing an error, or other logic here
295+
process.on('unhandledRejection', (reason, promise) => {
296+
console.log('Unhandled Rejection at:', promise, 'reason:', reason);
297+
// Application specific logging, throwing an error, or other logic here
290298
});
291299

292300
somePromise.then((res) => {
@@ -312,7 +320,7 @@ as would typically be the case for other `'unhandledRejection'` events. To
312320
address such failures, a non-operational
313321
[`.catch(() => { })`][`promise.catch()`] handler may be attached to
314322
`resource.loaded`, which would prevent the `'unhandledRejection'` event from
315-
being emitted. Alternatively, the [`'rejectionHandled'`][] event may be used.
323+
being emitted.
316324

317325
### Event: 'warning'
318326
<!-- YAML
@@ -2134,7 +2142,6 @@ cases:
21342142

21352143
[`'exit'`]: #process_event_exit
21362144
[`'message'`]: child_process.html#child_process_event_message
2137-
[`'rejectionHandled'`]: #process_event_rejectionhandled
21382145
[`'uncaughtException'`]: #process_event_uncaughtexception
21392146
[`ChildProcess.disconnect()`]: child_process.html#child_process_subprocess_disconnect
21402147
[`ChildProcess.send()`]: child_process.html#child_process_subprocess_send_message_sendhandle_options_callback

doc/node.1

+3
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ Print stack traces for process warnings (including deprecations).
218218
.It Fl -track-heap-objects
219219
Track heap object allocations for heap snapshots.
220220
.
221+
.It Fl --unhandled-rejections=mode
222+
Define the behavior for unhandled rejections. Can be one of `strict` (raise an error), `warn` (enforce warnings) or `none` (silence warnings).
223+
.
221224
.It Fl -use-bundled-ca , Fl -use-openssl-ca
222225
Use bundled Mozilla CA store as supplied by current Node.js version or use OpenSSL's default CA store.
223226
The default store is selectable at build-time.

lib/internal/bootstrap/node.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -486,14 +486,15 @@
486486
emitAfter
487487
} = NativeModule.require('internal/async_hooks');
488488

489-
process._fatalException = (er) => {
489+
process._fatalException = (er, fromPromise) => {
490490
// It's possible that defaultTriggerAsyncId was set for a constructor
491491
// call that threw and was never cleared. So clear it now.
492492
clearDefaultTriggerAsyncId();
493493

494+
const type = fromPromise ? 'unhandledRejection' : 'uncaughtException';
494495
if (exceptionHandlerState.captureFn !== null) {
495496
exceptionHandlerState.captureFn(er);
496-
} else if (!process.emit('uncaughtException', er)) {
497+
} else if (!process.emit('uncaughtException', er, type)) {
497498
// If someone handled it, then great. otherwise, die in C++ land
498499
// since that means that we'll exit the process, emit the 'exit' event.
499500
try {

lib/internal/process/promises.js

+74-16
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,24 @@ let lastPromiseId = 0;
1111
exports.setup = setupPromises;
1212

1313
function setupPromises(_setupPromises) {
14-
_setupPromises(handler, promiseRejectEvents);
14+
_setupPromises(promiseRejectHandler, promiseRejectEvents);
1515
return emitPromiseRejectionWarnings;
1616
}
1717

18-
function handler(type, promise, reason) {
18+
const states = {
19+
none: 0,
20+
warn: 1,
21+
strict: 2,
22+
default: 3
23+
};
24+
25+
let state;
26+
27+
function promiseRejectHandler(type, promise, reason) {
28+
if (state === undefined) {
29+
const { getOptionValue } = require('internal/options');
30+
state = states[getOptionValue('--unhandled-rejections') || 'default'];
31+
}
1932
switch (type) {
2033
case promiseRejectEvents.kPromiseRejectWithNoHandler:
2134
return unhandledRejection(promise, reason);
@@ -42,6 +55,7 @@ function unhandledRejection(promise, reason) {
4255
uid: ++lastPromiseId,
4356
warned: false
4457
});
58+
// This causes the promise to be referenced at least for one tick.
4559
pendingUnhandledRejections.push(promise);
4660
return true;
4761
}
@@ -67,14 +81,16 @@ function handledRejection(promise) {
6781

6882
const unhandledRejectionErrName = 'UnhandledPromiseRejectionWarning';
6983
function emitWarning(uid, reason) {
70-
// eslint-disable-next-line no-restricted-syntax
71-
const warning = new Error(
84+
if (state === states.none) {
85+
return;
86+
}
87+
const warning = getError(
88+
unhandledRejectionErrName,
7289
'Unhandled promise rejection. This error originated either by ' +
73-
'throwing inside of an async function without a catch block, ' +
74-
'or by rejecting a promise which was not handled with .catch(). ' +
75-
`(rejection id: ${uid})`
90+
'throwing inside of an async function without a catch block, ' +
91+
'or by rejecting a promise which was not handled with .catch(). ' +
92+
`(rejection id: ${uid})`
7693
);
77-
warning.name = unhandledRejectionErrName;
7894
try {
7995
if (reason instanceof Error) {
8096
warning.stack = reason.stack;
@@ -90,7 +106,7 @@ function emitWarning(uid, reason) {
90106

91107
let deprecationWarned = false;
92108
function emitDeprecationWarning() {
93-
if (!deprecationWarned) {
109+
if (state === states.default && !deprecationWarned) {
94110
deprecationWarned = true;
95111
process.emitWarning(
96112
'Unhandled promise rejections are deprecated. In the future, ' +
@@ -113,14 +129,56 @@ function emitPromiseRejectionWarnings() {
113129
while (len--) {
114130
const promise = pendingUnhandledRejections.shift();
115131
const promiseInfo = maybeUnhandledPromises.get(promise);
116-
if (promiseInfo !== undefined) {
117-
promiseInfo.warned = true;
118-
const { reason, uid } = promiseInfo;
119-
if (!process.emit('unhandledRejection', reason, promise)) {
120-
emitWarning(uid, reason);
121-
}
122-
maybeScheduledTicks = true;
132+
if (promiseInfo === undefined) {
133+
continue;
134+
}
135+
promiseInfo.warned = true;
136+
const { reason, uid } = promiseInfo;
137+
if (state === states.strict) {
138+
fatalException(reason);
123139
}
140+
if (!process.emit('unhandledRejection', reason, promise) ||
141+
// Always warn in case the user requested it.
142+
state === states.warn) {
143+
emitWarning(uid, reason);
144+
}
145+
maybeScheduledTicks = true;
124146
}
125147
return maybeScheduledTicks || pendingUnhandledRejections.length !== 0;
126148
}
149+
150+
function getError(name, message) {
151+
// Reset the stack to prevent any overhead.
152+
const tmp = Error.stackTraceLimit;
153+
Error.stackTraceLimit = 0;
154+
// eslint-disable-next-line no-restricted-syntax
155+
const err = new Error(message);
156+
Error.stackTraceLimit = tmp;
157+
Object.defineProperty(err, 'name', {
158+
value: name,
159+
enumerable: false,
160+
writable: true,
161+
configurable: true,
162+
});
163+
return err;
164+
}
165+
166+
function fatalException(reason) {
167+
let err;
168+
if (reason instanceof Error) {
169+
err = reason;
170+
} else {
171+
err = getError(
172+
'UnhandledPromiseRejection',
173+
'This error originated either by ' +
174+
'throwing inside of an async function without a catch block, ' +
175+
'or by rejecting a promise which was not handled with .catch().' +
176+
` The promise rejected with the reason "${safeToString(reason)}".`
177+
);
178+
err.code = 'ERR_UNHANDLED_REJECTION';
179+
}
180+
181+
if (!process._fatalException(err, true /* fromPromise */)) {
182+
throw err;
183+
}
184+
}

lib/internal/worker.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -479,11 +479,11 @@ function setupChild(evalScript) {
479479
originalFatalException = process._fatalException;
480480
process._fatalException = fatalException;
481481

482-
function fatalException(error) {
482+
function fatalException(error, fromPromise) {
483483
debug(`[${threadId}] gets fatal exception`);
484484
let caught = false;
485485
try {
486-
caught = originalFatalException.call(this, error);
486+
caught = originalFatalException.call(this, error, fromPromise);
487487
} catch (e) {
488488
error = e;
489489
}

src/node.cc

+11-2
Original file line numberDiff line numberDiff line change
@@ -1370,7 +1370,8 @@ FatalTryCatch::~FatalTryCatch() {
13701370

13711371
void FatalException(Isolate* isolate,
13721372
Local<Value> error,
1373-
Local<Message> message) {
1373+
Local<Message> message,
1374+
bool from_promise) {
13741375
HandleScope scope(isolate);
13751376

13761377
Environment* env = Environment::GetCurrent(isolate);
@@ -1391,9 +1392,12 @@ void FatalException(Isolate* isolate,
13911392
// Do not call FatalException when _fatalException handler throws
13921393
fatal_try_catch.SetVerbose(false);
13931394

1395+
Local<Value> argv[2] = { error,
1396+
Boolean::New(env->isolate(), from_promise) };
1397+
13941398
// This will return true if the JS layer handled it, false otherwise
13951399
MaybeLocal<Value> caught = fatal_exception_function.As<Function>()->Call(
1396-
env->context(), process_object, 1, &error);
1400+
env->context(), process_object, arraysize(argv), argv);
13971401

13981402
if (fatal_try_catch.HasTerminated())
13991403
return;
@@ -1418,6 +1422,11 @@ void FatalException(Isolate* isolate,
14181422
}
14191423
}
14201424

1425+
void FatalException(Isolate* isolate,
1426+
Local<Value> error,
1427+
Local<Message> message) {
1428+
FatalException(isolate, error, message, false /* from_promise */);
1429+
}
14211430

14221431
void FatalException(Isolate* isolate, const TryCatch& try_catch) {
14231432
// If we try to print out a termination exception, we'd just get 'null',

src/node_options.cc

+13
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) {
3939
if (syntax_check_only && has_eval_string) {
4040
errors->push_back("either --check or --eval can be used, not both");
4141
}
42+
43+
if (!unhandled_rejections.empty() &&
44+
unhandled_rejections != "strict" &&
45+
unhandled_rejections != "warn" &&
46+
unhandled_rejections != "none") {
47+
errors->push_back("invalid value for --unhandled-rejections");
48+
}
49+
4250
debug_options->CheckOptions(errors);
4351
}
4452

@@ -155,6 +163,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
155163
"show stack traces on process warnings",
156164
&EnvironmentOptions::trace_warnings,
157165
kAllowedInEnvironment);
166+
AddOption("--unhandled-rejections",
167+
"define unhandled rejections behavior. Options are 'strict' (raise "
168+
"an error), 'warn' (enforce warnings) or 'none' (silence warnings)",
169+
&EnvironmentOptions::unhandled_rejections,
170+
kAllowedInEnvironment);
158171

159172
AddOption("--check",
160173
"syntax check script without executing",

src/node_options.h

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class EnvironmentOptions : public Options {
8484
bool trace_deprecation = false;
8585
bool trace_sync_io = false;
8686
bool trace_warnings = false;
87+
std::string unhandled_rejections;
8788
std::string userland_loader;
8889

8990
bool syntax_check_only = false;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Flags: --unhandled-rejections=strict
2+
'use strict';
3+
4+
require('../common');
5+
6+
// Check that the process will exit on the first unhandled rejection in case the
7+
// unhandled rejections mode is set to `'strict'`.
8+
9+
const ref1 = new Promise(() => {
10+
throw new Error('One');
11+
});
12+
13+
const ref2 = Promise.reject(new Error('Two'));
14+
15+
// Keep the event loop alive to actually detect the unhandled rejection.
16+
setTimeout(() => console.log(ref1, ref2), 1000);

0 commit comments

Comments
 (0)