Skip to content

Commit 9dcc9b6

Browse files
committed
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 2755471 commit 9dcc9b6

25 files changed

+376
-50
lines changed

doc/api/cli.md

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

578578
Track heap object allocations for heap snapshots.
579579

580+
### `--unhandled-rejections=mode`
581+
<!-- YAML
582+
added: REPLACEME
583+
-->
584+
585+
By default all unhandled rejections trigger a warning plus a deprecation warning
586+
for the very first unhandled rejection in case no [`unhandledRejection`][] hook
587+
is used.
588+
589+
Using this flag allows to change what should happen when an unhandled rejection
590+
occurs. One of three modes can be chosen:
591+
592+
* `strict`: Raise the unhandled rejection as an uncaught exception.
593+
* `warn`: Always trigger a warning, no matter if the [`unhandledRejection`][]
594+
hook is set or not but do not print the deprecation warning.
595+
* `none`: Silence all warnings.
596+
580597
### `--use-bundled-ca`, `--use-openssl-ca`
581598
<!-- YAML
582599
added: v6.11.0
@@ -798,6 +815,7 @@ Node.js options that are allowed are:
798815
- `--trace-sync-io`
799816
- `--trace-warnings`
800817
- `--track-heap-objects`
818+
- `--unhandled-rejections`
801819
- `--use-bundled-ca`
802820
- `--use-openssl-ca`
803821
- `--v8-pool-size`
@@ -966,6 +984,7 @@ greater than `4` (its current default value). For more information, see the
966984
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
967985
[`tls.DEFAULT_MAX_VERSION`]: tls.html#tls_tls_default_max_version
968986
[`tls.DEFAULT_MIN_VERSION`]: tls.html#tls_tls_default_min_version
987+
[`unhandledRejection`]: process.html#process_event_unhandledrejection
969988
[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/
970989
[REPL]: repl.html
971990
[ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage

doc/api/process.md

+22-15
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,17 @@ most convenient for scripts).
205205
### Event: 'uncaughtException'
206206
<!-- YAML
207207
added: v0.1.18
208+
changes:
209+
- version: REPLACEME
210+
pr-url: https://github.com/nodejs/node/pull/26599
211+
description: Added the `origin` argument.
208212
-->
209213

214+
* `err` {Error} The uncaught exception.
215+
* `origin` {string} Indicates if the exception originates from an unhandled
216+
rejection or from synchronous errors. Can either be `'uncaughtException'` or
217+
`'unhandledRejection'`.
218+
210219
The `'uncaughtException'` event is emitted when an uncaught JavaScript
211220
exception bubbles all the way back to the event loop. By default, Node.js
212221
handles such exceptions by printing the stack trace to `stderr` and exiting
@@ -217,12 +226,13 @@ behavior. Alternatively, change the [`process.exitCode`][] in the
217226
provided exit code. Otherwise, in the presence of such handler the process will
218227
exit with 0.
219228

220-
The listener function is called with the `Error` object passed as the only
221-
argument.
222-
223229
```js
224-
process.on('uncaughtException', (err) => {
225-
fs.writeSync(1, `Caught exception: ${err}\n`);
230+
process.on('uncaughtException', (err, origin) => {
231+
fs.writeSync(
232+
process.stderr.fd,
233+
`Caught exception: ${err}\n` +
234+
`Exception origin: ${origin}`
235+
);
226236
});
227237

228238
setTimeout(() => {
@@ -274,6 +284,10 @@ changes:
274284
a process warning.
275285
-->
276286

287+
* `reason` {Error|any} The object with which the promise was rejected
288+
(typically an [`Error`][] object).
289+
* `promise` {Promise} The rejected promise.
290+
277291
The `'unhandledRejection'` event is emitted whenever a `Promise` is rejected and
278292
no error handler is attached to the promise within a turn of the event loop.
279293
When programming with Promises, exceptions are encapsulated as "rejected
@@ -282,15 +296,9 @@ are propagated through a `Promise` chain. The `'unhandledRejection'` event is
282296
useful for detecting and keeping track of promises that were rejected whose
283297
rejections have not yet been handled.
284298

285-
The listener function is called with the following arguments:
286-
287-
* `reason` {Error|any} The object with which the promise was rejected
288-
(typically an [`Error`][] object).
289-
* `p` the `Promise` that was rejected.
290-
291299
```js
292-
process.on('unhandledRejection', (reason, p) => {
293-
console.log('Unhandled Rejection at:', p, 'reason:', reason);
300+
process.on('unhandledRejection', (reason, promise) => {
301+
console.log('Unhandled Rejection at:', promise, 'reason:', reason);
294302
// Application specific logging, throwing an error, or other logic here
295303
});
296304

@@ -317,7 +325,7 @@ as would typically be the case for other `'unhandledRejection'` events. To
317325
address such failures, a non-operational
318326
[`.catch(() => { })`][`promise.catch()`] handler may be attached to
319327
`resource.loaded`, which would prevent the `'unhandledRejection'` event from
320-
being emitted. Alternatively, the [`'rejectionHandled'`][] event may be used.
328+
being emitted.
321329

322330
### Event: 'warning'
323331
<!-- YAML
@@ -2282,7 +2290,6 @@ cases:
22822290

22832291
[`'exit'`]: #process_event_exit
22842292
[`'message'`]: child_process.html#child_process_event_message
2285-
[`'rejectionHandled'`]: #process_event_rejectionhandled
22862293
[`'uncaughtException'`]: #process_event_uncaughtexception
22872294
[`ChildProcess.disconnect()`]: child_process.html#child_process_subprocess_disconnect
22882295
[`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
@@ -292,6 +292,9 @@ Print stack traces for process warnings (including deprecations).
292292
.It Fl -track-heap-objects
293293
Track heap object allocations for heap snapshots.
294294
.
295+
.It Fl --unhandled-rejections=mode
296+
Define the behavior for unhandled rejections. Can be one of `strict` (raise an error), `warn` (enforce warnings) or `none` (silence warnings).
297+
.
295298
.It Fl -use-bundled-ca , Fl -use-openssl-ca
296299
Use bundled Mozilla CA store as supplied by current Node.js version or use OpenSSL's default CA store.
297300
The default store is selectable at build-time.

lib/internal/main/worker_thread.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,11 @@ port.on('message', (message) => {
137137
});
138138

139139
// Overwrite fatalException
140-
process._fatalException = (error) => {
140+
process._fatalException = (error, fromPromise) => {
141141
debug(`[${threadId}] gets fatal exception`);
142142
let caught = false;
143143
try {
144-
caught = originalFatalException.call(this, error);
144+
caught = originalFatalException.call(this, error, fromPromise);
145145
} catch (e) {
146146
error = e;
147147
}

lib/internal/modules/cjs/loader.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -821,7 +821,10 @@ Module.runMain = function() {
821821
return loader.import(pathToFileURL(process.argv[1]).pathname);
822822
})
823823
.catch((e) => {
824-
internalBinding('task_queue').triggerFatalException(e);
824+
internalBinding('task_queue').triggerFatalException(
825+
e,
826+
true /* fromPromise */
827+
);
825828
});
826829
// Handle any nextTicks added in the first tick of the program
827830
process._tickCallback();

lib/internal/process/execution.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ function noop() {}
117117
// before calling into process._fatalException, or this function should
118118
// take extra care of the async hooks before it schedules a setImmediate.
119119
function createFatalException() {
120-
return (er) => {
120+
return (er, fromPromise) => {
121121
// It's possible that defaultTriggerAsyncId was set for a constructor
122122
// call that threw and was never cleared. So clear it now.
123123
clearDefaultTriggerAsyncId();
@@ -138,9 +138,10 @@ function createFatalException() {
138138
} catch {} // Ignore the exception. Diagnostic reporting is unavailable.
139139
}
140140

141+
const type = fromPromise ? 'unhandledRejection' : 'uncaughtException';
141142
if (exceptionHandlerState.captureFn !== null) {
142143
exceptionHandlerState.captureFn(er);
143-
} else if (!process.emit('uncaughtException', er)) {
144+
} else if (!process.emit('uncaughtException', er, type)) {
144145
// If someone handled it, then great. otherwise, die in C++ land
145146
// since that means that we'll exit the process, emit the 'exit' event.
146147
try {

lib/internal/process/promises.js

+76-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
'use strict';
22

3-
const { safeToString } = internalBinding('util');
3+
const { Object } = primordials;
4+
5+
const {
6+
safeToString
7+
} = internalBinding('util');
48
const {
59
tickInfo,
610
promiseRejectEvents: {
@@ -9,7 +13,8 @@ const {
913
kPromiseResolveAfterResolved,
1014
kPromiseRejectAfterResolved
1115
},
12-
setPromiseRejectCallback
16+
setPromiseRejectCallback,
17+
triggerFatalException
1318
} = internalBinding('task_queue');
1419

1520
// *Must* match Environment::TickInfo::Fields in src/env.h.
@@ -20,6 +25,15 @@ const pendingUnhandledRejections = [];
2025
const asyncHandledRejections = [];
2126
let lastPromiseId = 0;
2227

28+
const states = {
29+
none: 0,
30+
warn: 1,
31+
strict: 2,
32+
default: 3
33+
};
34+
35+
let state;
36+
2337
function setHasRejectionToWarn(value) {
2438
tickInfo[kHasRejectionToWarn] = value ? 1 : 0;
2539
}
@@ -29,6 +43,10 @@ function hasRejectionToWarn() {
2943
}
3044

3145
function promiseRejectHandler(type, promise, reason) {
46+
if (state === undefined) {
47+
const { getOptionValue } = require('internal/options');
48+
state = states[getOptionValue('--unhandled-rejections') || 'default'];
49+
}
3250
switch (type) {
3351
case kPromiseRejectWithNoHandler:
3452
unhandledRejection(promise, reason);
@@ -59,6 +77,7 @@ function unhandledRejection(promise, reason) {
5977
uid: ++lastPromiseId,
6078
warned: false
6179
});
80+
// This causes the promise to be referenced at least for one tick.
6281
pendingUnhandledRejections.push(promise);
6382
setHasRejectionToWarn(true);
6483
}
@@ -85,14 +104,16 @@ function handledRejection(promise) {
85104

86105
const unhandledRejectionErrName = 'UnhandledPromiseRejectionWarning';
87106
function emitWarning(uid, reason) {
88-
// eslint-disable-next-line no-restricted-syntax
89-
const warning = new Error(
107+
if (state === states.none) {
108+
return;
109+
}
110+
const warning = getError(
111+
unhandledRejectionErrName,
90112
'Unhandled promise rejection. This error originated either by ' +
91-
'throwing inside of an async function without a catch block, ' +
92-
'or by rejecting a promise which was not handled with .catch(). ' +
93-
`(rejection id: ${uid})`
113+
'throwing inside of an async function without a catch block, ' +
114+
'or by rejecting a promise which was not handled with .catch(). ' +
115+
`(rejection id: ${uid})`
94116
);
95-
warning.name = unhandledRejectionErrName;
96117
try {
97118
if (reason instanceof Error) {
98119
warning.stack = reason.stack;
@@ -108,7 +129,7 @@ function emitWarning(uid, reason) {
108129

109130
let deprecationWarned = false;
110131
function emitDeprecationWarning() {
111-
if (!deprecationWarned) {
132+
if (state === states.default && !deprecationWarned) {
112133
deprecationWarned = true;
113134
process.emitWarning(
114135
'Unhandled promise rejections are deprecated. In the future, ' +
@@ -133,18 +154,57 @@ function processPromiseRejections() {
133154
while (len--) {
134155
const promise = pendingUnhandledRejections.shift();
135156
const promiseInfo = maybeUnhandledPromises.get(promise);
136-
if (promiseInfo !== undefined) {
137-
promiseInfo.warned = true;
138-
const { reason, uid } = promiseInfo;
139-
if (!process.emit('unhandledRejection', reason, promise)) {
140-
emitWarning(uid, reason);
141-
}
142-
maybeScheduledTicks = true;
157+
if (promiseInfo === undefined) {
158+
continue;
159+
}
160+
promiseInfo.warned = true;
161+
const { reason, uid } = promiseInfo;
162+
if (state === states.strict) {
163+
fatalException(reason);
143164
}
165+
if (!process.emit('unhandledRejection', reason, promise) ||
166+
// Always warn in case the user requested it.
167+
state === states.warn) {
168+
emitWarning(uid, reason);
169+
}
170+
maybeScheduledTicks = true;
144171
}
145172
return maybeScheduledTicks || pendingUnhandledRejections.length !== 0;
146173
}
147174

175+
function getError(name, message) {
176+
// Reset the stack to prevent any overhead.
177+
const tmp = Error.stackTraceLimit;
178+
Error.stackTraceLimit = 0;
179+
// eslint-disable-next-line no-restricted-syntax
180+
const err = new Error(message);
181+
Error.stackTraceLimit = tmp;
182+
Object.defineProperty(err, 'name', {
183+
value: name,
184+
enumerable: false,
185+
writable: true,
186+
configurable: true,
187+
});
188+
return err;
189+
}
190+
191+
function fatalException(reason) {
192+
let err;
193+
if (reason instanceof Error) {
194+
err = reason;
195+
} else {
196+
err = getError(
197+
'UnhandledPromiseRejection',
198+
'This error originated either by ' +
199+
'throwing inside of an async function without a catch block, ' +
200+
'or by rejecting a promise which was not handled with .catch().' +
201+
` The promise rejected with the reason "${safeToString(reason)}".`
202+
);
203+
err.code = 'ERR_UNHANDLED_REJECTION';
204+
}
205+
triggerFatalException(err, true /* fromPromise */);
206+
}
207+
148208
function listenForRejections() {
149209
setPromiseRejectCallback(promiseRejectHandler);
150210
}

lib/internal/process/task_queues.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,11 @@ function runMicrotask() {
155155
try {
156156
callback();
157157
} catch (error) {
158-
// TODO(devsnek) remove this if
158+
// TODO(devsnek): Remove this if
159159
// https://bugs.chromium.org/p/v8/issues/detail?id=8326
160160
// is resolved such that V8 triggers the fatal exception
161-
// handler for microtasks
162-
triggerFatalException(error);
161+
// handler for microtasks.
162+
triggerFatalException(error, false /* fromPromise */);
163163
} finally {
164164
this.emitDestroy();
165165
}

src/node_errors.cc

+13-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
namespace node {
1111

1212
using errors::TryCatchScope;
13+
using v8::Boolean;
1314
using v8::Context;
1415
using v8::Exception;
1516
using v8::Function;
@@ -771,7 +772,8 @@ void DecorateErrorStack(Environment* env,
771772

772773
void FatalException(Isolate* isolate,
773774
Local<Value> error,
774-
Local<Message> message) {
775+
Local<Message> message,
776+
bool from_promise) {
775777
CHECK(!error.IsEmpty());
776778
HandleScope scope(isolate);
777779

@@ -794,9 +796,12 @@ void FatalException(Isolate* isolate,
794796
// Do not call FatalException when _fatalException handler throws
795797
fatal_try_catch.SetVerbose(false);
796798

799+
Local<Value> argv[2] = { error,
800+
Boolean::New(env->isolate(), from_promise) };
801+
797802
// This will return true if the JS layer handled it, false otherwise
798803
MaybeLocal<Value> caught = fatal_exception_function.As<Function>()->Call(
799-
env->context(), process_object, 1, &error);
804+
env->context(), process_object, arraysize(argv), argv);
800805

801806
if (fatal_try_catch.HasTerminated()) return;
802807

@@ -821,4 +826,10 @@ void FatalException(Isolate* isolate,
821826
}
822827
}
823828

829+
void FatalException(Isolate* isolate,
830+
Local<Value> error,
831+
Local<Message> message) {
832+
FatalException(isolate, error, message, false /* from_promise */);
833+
}
834+
824835
} // namespace node

0 commit comments

Comments
 (0)