Skip to content

Commit

Permalink
process: add --unhandled-rejections flag
Browse files Browse the repository at this point in the history
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 <benjamingr@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Matheus Marchini <mat@mmarchini.me>
Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com>
Reviewed-By: Sakthipriyan Vairamani <thechargingvolcano@gmail.com>
  • Loading branch information
BridgeAR authored and BethGriggs committed Oct 15, 2019
1 parent c285e69 commit b43d7e8
Show file tree
Hide file tree
Showing 23 changed files with 365 additions and 47 deletions.
19 changes: 19 additions & 0 deletions doc/api/cli.md
Expand Up @@ -401,6 +401,23 @@ added: v2.4.0

Track heap object allocations for heap snapshots.

### `--unhandled-rejections=mode`
<!-- YAML
added: REPLACEME
-->

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
occurs. One of three modes can be chosen:

* `strict`: Raise the unhandled rejection as an uncaught exception.
* `warn`: Always trigger a warning, no matter if the [`unhandledRejection`][]
hook is set or not but do not print the deprecation warning.
* `none`: Silence all warnings.

### `--use-bundled-ca`, `--use-openssl-ca`
<!-- YAML
added: v6.11.0
Expand Down Expand Up @@ -614,6 +631,7 @@ Node.js options that are allowed are:
- `--trace-sync-io`
- `--trace-warnings`
- `--track-heap-objects`
- `--unhandled-rejections`
- `--use-bundled-ca`
- `--use-openssl-ca`
- `--v8-pool-size`
Expand Down Expand Up @@ -773,6 +791,7 @@ greater than `4` (its current default value). For more information, see the
[`Buffer`]: buffer.html#buffer_class_buffer
[`SlowBuffer`]: buffer.html#buffer_class_slowbuffer
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
[`unhandledRejection`]: process.html#process_event_unhandledrejection
[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/
[REPL]: repl.html
[ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage
Expand Down
39 changes: 23 additions & 16 deletions doc/api/process.md
Expand Up @@ -200,8 +200,17 @@ most convenient for scripts).
### Event: 'uncaughtException'
<!-- YAML
added: v0.1.18
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/26599
description: Added the `origin` argument.
-->

* `err` {Error} The uncaught exception.
* `origin` {string} Indicates if the exception originates from an unhandled
rejection or from synchronous errors. Can either be `'uncaughtException'` or
`'unhandledRejection'`.

The `'uncaughtException'` event is emitted when an uncaught JavaScript
exception bubbles all the way back to the event loop. By default, Node.js
handles such exceptions by printing the stack trace to `stderr` and exiting
Expand All @@ -212,12 +221,13 @@ behavior. Alternatively, change the [`process.exitCode`][] in the
provided exit code. Otherwise, in the presence of such handler the process will
exit with 0.

The listener function is called with the `Error` object passed as the only
argument.

```js
process.on('uncaughtException', (err) => {
fs.writeSync(1, `Caught exception: ${err}\n`);
process.on('uncaughtException', (err, origin) => {
fs.writeSync(
process.stderr.fd,
`Caught exception: ${err}\n` +
`Exception origin: ${origin}`
);
});

setTimeout(() => {
Expand Down Expand Up @@ -269,6 +279,10 @@ changes:
a process warning.
-->

* `reason` {Error|any} The object with which the promise was rejected
(typically an [`Error`][] object).
* `promise` {Promise} The rejected promise.

The `'unhandledRejection'` event is emitted whenever a `Promise` is rejected and
no error handler is attached to the promise within a turn of the event loop.
When programming with Promises, exceptions are encapsulated as "rejected
Expand All @@ -277,16 +291,10 @@ are propagated through a `Promise` chain. The `'unhandledRejection'` event is
useful for detecting and keeping track of promises that were rejected whose
rejections have not yet been handled.

The listener function is called with the following arguments:

* `reason` {Error|any} The object with which the promise was rejected
(typically an [`Error`][] object).
* `p` the `Promise` that was rejected.

```js
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at:', p, 'reason:', reason);
// application specific logging, throwing an error, or other logic here
process.on('unhandledRejection', (reason, promise) => {
console.log('Unhandled Rejection at:', promise, 'reason:', reason);
// Application specific logging, throwing an error, or other logic here
});

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

### Event: 'warning'
<!-- YAML
Expand Down Expand Up @@ -2134,7 +2142,6 @@ cases:

[`'exit'`]: #process_event_exit
[`'message'`]: child_process.html#child_process_event_message
[`'rejectionHandled'`]: #process_event_rejectionhandled
[`'uncaughtException'`]: #process_event_uncaughtexception
[`ChildProcess.disconnect()`]: child_process.html#child_process_subprocess_disconnect
[`ChildProcess.send()`]: child_process.html#child_process_subprocess_send_message_sendhandle_options_callback
Expand Down
3 changes: 3 additions & 0 deletions doc/node.1
Expand Up @@ -218,6 +218,9 @@ Print stack traces for process warnings (including deprecations).
.It Fl -track-heap-objects
Track heap object allocations for heap snapshots.
.
.It Fl --unhandled-rejections=mode
Define the behavior for unhandled rejections. Can be one of `strict` (raise an error), `warn` (enforce warnings) or `none` (silence warnings).
.
.It Fl -use-bundled-ca , Fl -use-openssl-ca
Use bundled Mozilla CA store as supplied by current Node.js version or use OpenSSL's default CA store.
The default store is selectable at build-time.
Expand Down
5 changes: 3 additions & 2 deletions lib/internal/bootstrap/node.js
Expand Up @@ -486,14 +486,15 @@
emitAfter
} = NativeModule.require('internal/async_hooks');

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

const type = fromPromise ? 'unhandledRejection' : 'uncaughtException';
if (exceptionHandlerState.captureFn !== null) {
exceptionHandlerState.captureFn(er);
} else if (!process.emit('uncaughtException', er)) {
} else if (!process.emit('uncaughtException', er, type)) {
// If someone handled it, then great. otherwise, die in C++ land
// since that means that we'll exit the process, emit the 'exit' event.
try {
Expand Down
90 changes: 74 additions & 16 deletions lib/internal/process/promises.js
Expand Up @@ -11,11 +11,24 @@ let lastPromiseId = 0;
exports.setup = setupPromises;

function setupPromises(_setupPromises) {
_setupPromises(handler, promiseRejectEvents);
_setupPromises(promiseRejectHandler, promiseRejectEvents);
return emitPromiseRejectionWarnings;
}

function handler(type, promise, reason) {
const states = {
none: 0,
warn: 1,
strict: 2,
default: 3
};

let state;

function promiseRejectHandler(type, promise, reason) {
if (state === undefined) {
const { getOptionValue } = require('internal/options');
state = states[getOptionValue('--unhandled-rejections') || 'default'];
}
switch (type) {
case promiseRejectEvents.kPromiseRejectWithNoHandler:
return unhandledRejection(promise, reason);
Expand All @@ -42,6 +55,7 @@ function unhandledRejection(promise, reason) {
uid: ++lastPromiseId,
warned: false
});
// This causes the promise to be referenced at least for one tick.
pendingUnhandledRejections.push(promise);
return true;
}
Expand All @@ -67,14 +81,16 @@ function handledRejection(promise) {

const unhandledRejectionErrName = 'UnhandledPromiseRejectionWarning';
function emitWarning(uid, reason) {
// eslint-disable-next-line no-restricted-syntax
const warning = new Error(
if (state === states.none) {
return;
}
const warning = getError(
unhandledRejectionErrName,
'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(). ' +
`(rejection id: ${uid})`
'throwing inside of an async function without a catch block, ' +
'or by rejecting a promise which was not handled with .catch(). ' +
`(rejection id: ${uid})`
);
warning.name = unhandledRejectionErrName;
try {
if (reason instanceof Error) {
warning.stack = reason.stack;
Expand All @@ -90,7 +106,7 @@ function emitWarning(uid, reason) {

let deprecationWarned = false;
function emitDeprecationWarning() {
if (!deprecationWarned) {
if (state === states.default && !deprecationWarned) {
deprecationWarned = true;
process.emitWarning(
'Unhandled promise rejections are deprecated. In the future, ' +
Expand All @@ -113,14 +129,56 @@ function emitPromiseRejectionWarnings() {
while (len--) {
const promise = pendingUnhandledRejections.shift();
const promiseInfo = maybeUnhandledPromises.get(promise);
if (promiseInfo !== undefined) {
promiseInfo.warned = true;
const { reason, uid } = promiseInfo;
if (!process.emit('unhandledRejection', reason, promise)) {
emitWarning(uid, reason);
}
maybeScheduledTicks = true;
if (promiseInfo === undefined) {
continue;
}
promiseInfo.warned = true;
const { reason, uid } = promiseInfo;
if (state === states.strict) {
fatalException(reason);
}
if (!process.emit('unhandledRejection', reason, promise) ||
// Always warn in case the user requested it.
state === states.warn) {
emitWarning(uid, reason);
}
maybeScheduledTicks = true;
}
return maybeScheduledTicks || pendingUnhandledRejections.length !== 0;
}

function getError(name, message) {
// Reset the stack to prevent any overhead.
const tmp = Error.stackTraceLimit;
Error.stackTraceLimit = 0;
// eslint-disable-next-line no-restricted-syntax
const err = new Error(message);
Error.stackTraceLimit = tmp;
Object.defineProperty(err, 'name', {
value: name,
enumerable: false,
writable: true,
configurable: true,
});
return err;
}

function fatalException(reason) {
let err;
if (reason instanceof Error) {
err = reason;
} else {
err = getError(
'UnhandledPromiseRejection',
'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 "${safeToString(reason)}".`
);
err.code = 'ERR_UNHANDLED_REJECTION';
}

if (!process._fatalException(err, true /* fromPromise */)) {
throw err;
}
}
4 changes: 2 additions & 2 deletions lib/internal/worker.js
Expand Up @@ -479,11 +479,11 @@ function setupChild(evalScript) {
originalFatalException = process._fatalException;
process._fatalException = fatalException;

function fatalException(error) {
function fatalException(error, fromPromise) {
debug(`[${threadId}] gets fatal exception`);
let caught = false;
try {
caught = originalFatalException.call(this, error);
caught = originalFatalException.call(this, error, fromPromise);
} catch (e) {
error = e;
}
Expand Down
13 changes: 11 additions & 2 deletions src/node.cc
Expand Up @@ -1370,7 +1370,8 @@ FatalTryCatch::~FatalTryCatch() {

void FatalException(Isolate* isolate,
Local<Value> error,
Local<Message> message) {
Local<Message> message,
bool from_promise) {
HandleScope scope(isolate);

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

Local<Value> argv[2] = { error,
Boolean::New(env->isolate(), from_promise) };

// This will return true if the JS layer handled it, false otherwise
MaybeLocal<Value> caught = fatal_exception_function.As<Function>()->Call(
env->context(), process_object, 1, &error);
env->context(), process_object, arraysize(argv), argv);

if (fatal_try_catch.HasTerminated())
return;
Expand All @@ -1418,6 +1422,11 @@ void FatalException(Isolate* isolate,
}
}

void FatalException(Isolate* isolate,
Local<Value> error,
Local<Message> message) {
FatalException(isolate, error, message, false /* from_promise */);
}

void FatalException(Isolate* isolate, const TryCatch& try_catch) {
// If we try to print out a termination exception, we'd just get 'null',
Expand Down
13 changes: 13 additions & 0 deletions src/node_options.cc
Expand Up @@ -39,6 +39,14 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) {
if (syntax_check_only && has_eval_string) {
errors->push_back("either --check or --eval can be used, not both");
}

if (!unhandled_rejections.empty() &&
unhandled_rejections != "strict" &&
unhandled_rejections != "warn" &&
unhandled_rejections != "none") {
errors->push_back("invalid value for --unhandled-rejections");
}

debug_options->CheckOptions(errors);
}

Expand Down Expand Up @@ -155,6 +163,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"show stack traces on process warnings",
&EnvironmentOptions::trace_warnings,
kAllowedInEnvironment);
AddOption("--unhandled-rejections",
"define unhandled rejections behavior. Options are 'strict' (raise "
"an error), 'warn' (enforce warnings) or 'none' (silence warnings)",
&EnvironmentOptions::unhandled_rejections,
kAllowedInEnvironment);

AddOption("--check",
"syntax check script without executing",
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Expand Up @@ -84,6 +84,7 @@ class EnvironmentOptions : public Options {
bool trace_deprecation = false;
bool trace_sync_io = false;
bool trace_warnings = false;
std::string unhandled_rejections;
std::string userland_loader;

bool syntax_check_only = false;
Expand Down
16 changes: 16 additions & 0 deletions test/message/promise_always_throw_unhandled.js
@@ -0,0 +1,16 @@
// Flags: --unhandled-rejections=strict
'use strict';

require('../common');

// Check that the process will exit on the first unhandled rejection in case the
// unhandled rejections mode is set to `'strict'`.

const ref1 = new Promise(() => {
throw new Error('One');
});

const ref2 = Promise.reject(new Error('Two'));

// Keep the event loop alive to actually detect the unhandled rejection.
setTimeout(() => console.log(ref1, ref2), 1000);

0 comments on commit b43d7e8

Please sign in to comment.