summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuben Bridgewater <ruben@bridgewater.de>2019-03-11 20:46:43 +0100
committerRuben Bridgewater <ruben@bridgewater.de>2019-04-15 18:29:07 +0200
commit9dcc9b6a6b3b8ab40be91b2fdc6fdf514e48dcc3 (patch)
treeb715c7140d05728acc5237d12b22eee7e2d0caff
parent2755471bf3ce35a14cb348d4fbf0d34779426e66 (diff)
downloadandroid-node-v8-9dcc9b6a6b3b8ab40be91b2fdc6fdf514e48dcc3.tar.gz
android-node-v8-9dcc9b6a6b3b8ab40be91b2fdc6fdf514e48dcc3.tar.bz2
android-node-v8-9dcc9b6a6b3b8ab40be91b2fdc6fdf514e48dcc3.zip
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: https://github.com/nodejs/node/pull/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>
-rw-r--r--doc/api/cli.md19
-rw-r--r--doc/api/process.md37
-rw-r--r--doc/node.13
-rw-r--r--lib/internal/main/worker_thread.js4
-rw-r--r--lib/internal/modules/cjs/loader.js5
-rw-r--r--lib/internal/process/execution.js5
-rw-r--r--lib/internal/process/promises.js92
-rw-r--r--lib/internal/process/task_queues.js6
-rw-r--r--src/node_errors.cc15
-rw-r--r--src/node_errors.h5
-rw-r--r--src/node_options.cc18
-rw-r--r--src/node_options.h1
-rw-r--r--src/node_task_queue.cc3
-rw-r--r--test/message/promise_always_throw_unhandled.js16
-rw-r--r--test/message/promise_always_throw_unhandled.out13
-rw-r--r--test/parallel/test-cli-node-options.js1
-rw-r--r--test/parallel/test-next-tick-errors.js4
-rw-r--r--test/parallel/test-promise-unhandled-error.js54
-rw-r--r--test/parallel/test-promise-unhandled-flag.js16
-rw-r--r--test/parallel/test-promise-unhandled-silent-no-hook.js22
-rw-r--r--test/parallel/test-promise-unhandled-silent.js23
-rw-r--r--test/parallel/test-promise-unhandled-warn-no-hook.js23
-rw-r--r--test/parallel/test-promise-unhandled-warn.js28
-rw-r--r--test/parallel/test-timers-immediate-queue-throw.js7
-rw-r--r--test/parallel/test-timers-unref-throw-then-ref.js6
25 files changed, 376 insertions, 50 deletions
diff --git a/doc/api/cli.md b/doc/api/cli.md
index 60d75a6c04..f56eb981a9 100644
--- a/doc/api/cli.md
+++ b/doc/api/cli.md
@@ -577,6 +577,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
@@ -798,6 +815,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`
@@ -966,6 +984,7 @@ greater than `4` (its current default value). For more information, see the
[`process.setUncaughtExceptionCaptureCallback()`]: process.html#process_process_setuncaughtexceptioncapturecallback_fn
[`tls.DEFAULT_MAX_VERSION`]: tls.html#tls_tls_default_max_version
[`tls.DEFAULT_MIN_VERSION`]: tls.html#tls_tls_default_min_version
+[`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
diff --git a/doc/api/process.md b/doc/api/process.md
index 5eabb6fad1..95f57f6932 100644
--- a/doc/api/process.md
+++ b/doc/api/process.md
@@ -205,8 +205,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
@@ -217,12 +226,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(() => {
@@ -274,6 +284,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
@@ -282,15 +296,9 @@ 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);
+process.on('unhandledRejection', (reason, promise) => {
+ console.log('Unhandled Rejection at:', promise, 'reason:', reason);
// Application specific logging, throwing an error, or other logic here
});
@@ -317,7 +325,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
@@ -2282,7 +2290,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
diff --git a/doc/node.1 b/doc/node.1
index 0fccabc4bc..e2b52b9e87 100644
--- a/doc/node.1
+++ b/doc/node.1
@@ -292,6 +292,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.
diff --git a/lib/internal/main/worker_thread.js b/lib/internal/main/worker_thread.js
index 0e463dc935..f290a4acb2 100644
--- a/lib/internal/main/worker_thread.js
+++ b/lib/internal/main/worker_thread.js
@@ -137,11 +137,11 @@ port.on('message', (message) => {
});
// Overwrite fatalException
-process._fatalException = (error) => {
+process._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;
}
diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js
index 5d0e777774..6d14c56db5 100644
--- a/lib/internal/modules/cjs/loader.js
+++ b/lib/internal/modules/cjs/loader.js
@@ -821,7 +821,10 @@ Module.runMain = function() {
return loader.import(pathToFileURL(process.argv[1]).pathname);
})
.catch((e) => {
- internalBinding('task_queue').triggerFatalException(e);
+ internalBinding('task_queue').triggerFatalException(
+ e,
+ true /* fromPromise */
+ );
});
// Handle any nextTicks added in the first tick of the program
process._tickCallback();
diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js
index 5712b80eaf..7b651ba5d1 100644
--- a/lib/internal/process/execution.js
+++ b/lib/internal/process/execution.js
@@ -117,7 +117,7 @@ function noop() {}
// before calling into process._fatalException, or this function should
// take extra care of the async hooks before it schedules a setImmediate.
function createFatalException() {
- return (er) => {
+ return (er, fromPromise) => {
// It's possible that defaultTriggerAsyncId was set for a constructor
// call that threw and was never cleared. So clear it now.
clearDefaultTriggerAsyncId();
@@ -138,9 +138,10 @@ function createFatalException() {
} catch {} // Ignore the exception. Diagnostic reporting is unavailable.
}
+ 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 {
diff --git a/lib/internal/process/promises.js b/lib/internal/process/promises.js
index f3e118e2ce..95161ac7a8 100644
--- a/lib/internal/process/promises.js
+++ b/lib/internal/process/promises.js
@@ -1,6 +1,10 @@
'use strict';
-const { safeToString } = internalBinding('util');
+const { Object } = primordials;
+
+const {
+ safeToString
+} = internalBinding('util');
const {
tickInfo,
promiseRejectEvents: {
@@ -9,7 +13,8 @@ const {
kPromiseResolveAfterResolved,
kPromiseRejectAfterResolved
},
- setPromiseRejectCallback
+ setPromiseRejectCallback,
+ triggerFatalException
} = internalBinding('task_queue');
// *Must* match Environment::TickInfo::Fields in src/env.h.
@@ -20,6 +25,15 @@ const pendingUnhandledRejections = [];
const asyncHandledRejections = [];
let lastPromiseId = 0;
+const states = {
+ none: 0,
+ warn: 1,
+ strict: 2,
+ default: 3
+};
+
+let state;
+
function setHasRejectionToWarn(value) {
tickInfo[kHasRejectionToWarn] = value ? 1 : 0;
}
@@ -29,6 +43,10 @@ function hasRejectionToWarn() {
}
function promiseRejectHandler(type, promise, reason) {
+ if (state === undefined) {
+ const { getOptionValue } = require('internal/options');
+ state = states[getOptionValue('--unhandled-rejections') || 'default'];
+ }
switch (type) {
case kPromiseRejectWithNoHandler:
unhandledRejection(promise, reason);
@@ -59,6 +77,7 @@ function unhandledRejection(promise, reason) {
uid: ++lastPromiseId,
warned: false
});
+ // This causes the promise to be referenced at least for one tick.
pendingUnhandledRejections.push(promise);
setHasRejectionToWarn(true);
}
@@ -85,14 +104,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;
@@ -108,7 +129,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, ' +
@@ -133,18 +154,57 @@ function processPromiseRejections() {
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';
+ }
+ triggerFatalException(err, true /* fromPromise */);
+}
+
function listenForRejections() {
setPromiseRejectCallback(promiseRejectHandler);
}
diff --git a/lib/internal/process/task_queues.js b/lib/internal/process/task_queues.js
index bc65d25307..12e34b7ff7 100644
--- a/lib/internal/process/task_queues.js
+++ b/lib/internal/process/task_queues.js
@@ -155,11 +155,11 @@ function runMicrotask() {
try {
callback();
} catch (error) {
- // TODO(devsnek) remove this if
+ // TODO(devsnek): Remove this if
// https://bugs.chromium.org/p/v8/issues/detail?id=8326
// is resolved such that V8 triggers the fatal exception
- // handler for microtasks
- triggerFatalException(error);
+ // handler for microtasks.
+ triggerFatalException(error, false /* fromPromise */);
} finally {
this.emitDestroy();
}
diff --git a/src/node_errors.cc b/src/node_errors.cc
index 3c04152974..83dbfc611f 100644
--- a/src/node_errors.cc
+++ b/src/node_errors.cc
@@ -10,6 +10,7 @@
namespace node {
using errors::TryCatchScope;
+using v8::Boolean;
using v8::Context;
using v8::Exception;
using v8::Function;
@@ -771,7 +772,8 @@ void DecorateErrorStack(Environment* env,
void FatalException(Isolate* isolate,
Local<Value> error,
- Local<Message> message) {
+ Local<Message> message,
+ bool from_promise) {
CHECK(!error.IsEmpty());
HandleScope scope(isolate);
@@ -794,9 +796,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;
@@ -821,4 +826,10 @@ void FatalException(Isolate* isolate,
}
}
+void FatalException(Isolate* isolate,
+ Local<Value> error,
+ Local<Message> message) {
+ FatalException(isolate, error, message, false /* from_promise */);
+}
+
} // namespace node
diff --git a/src/node_errors.h b/src/node_errors.h
index b04a347f1e..c27f4b36fa 100644
--- a/src/node_errors.h
+++ b/src/node_errors.h
@@ -39,6 +39,11 @@ void FatalException(v8::Isolate* isolate,
Local<Value> error,
Local<Message> message);
+void FatalException(v8::Isolate* isolate,
+ Local<Value> error,
+ Local<Message> message,
+ bool from_promise);
+
// Helpers to construct errors similar to the ones provided by
// lib/internal/errors.js.
// Example: with `V(ERR_INVALID_ARG_TYPE, TypeError)`, there will be
diff --git a/src/node_options.cc b/src/node_options.cc
index c307c66a62..cb490dad75 100644
--- a/src/node_options.cc
+++ b/src/node_options.cc
@@ -141,6 +141,13 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) {
errors->push_back("invalid value for --http-parser");
}
+ if (!unhandled_rejections.empty() &&
+ unhandled_rejections != "strict" &&
+ unhandled_rejections != "warn" &&
+ unhandled_rejections != "none") {
+ errors->push_back("invalid value for --unhandled-rejections");
+ }
+
#if HAVE_INSPECTOR
debug_options_.CheckOptions(errors);
#endif // HAVE_INSPECTOR
@@ -287,6 +294,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"custom loader",
&EnvironmentOptions::userland_loader,
kAllowedInEnvironment);
+ AddOption("--entry-type",
+ "set module type name of the entry point",
+ &EnvironmentOptions::module_type,
+ kAllowedInEnvironment);
AddOption("--es-module-specifier-resolution",
"Select extension resolution algorithm for es modules; "
"either 'explicit' (default) or 'node'",
@@ -342,9 +353,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"show stack traces on process warnings",
&EnvironmentOptions::trace_warnings,
kAllowedInEnvironment);
- AddOption("--entry-type",
- "set module type name of the entry point",
- &EnvironmentOptions::module_type,
+ 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",
diff --git a/src/node_options.h b/src/node_options.h
index 59baea562a..0fb480b397 100644
--- a/src/node_options.h
+++ b/src/node_options.h
@@ -114,6 +114,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;
diff --git a/src/node_task_queue.cc b/src/node_task_queue.cc
index b832a66d8b..b125f5d01e 100644
--- a/src/node_task_queue.cc
+++ b/src/node_task_queue.cc
@@ -116,7 +116,8 @@ static void TriggerFatalException(const FunctionCallbackInfo<Value>& args) {
ReportException(env, exception, message);
Abort();
}
- FatalException(isolate, exception, message);
+ bool from_promise = args[1]->IsTrue();
+ FatalException(isolate, exception, message, from_promise);
}
static void Initialize(Local<Object> target,
diff --git a/test/message/promise_always_throw_unhandled.js b/test/message/promise_always_throw_unhandled.js
new file mode 100644
index 0000000000..d9128f34a5
--- /dev/null
+++ b/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);
diff --git a/test/message/promise_always_throw_unhandled.out b/test/message/promise_always_throw_unhandled.out
new file mode 100644
index 0000000000..8d03026856
--- /dev/null
+++ b/test/message/promise_always_throw_unhandled.out
@@ -0,0 +1,13 @@
+*promise_always_throw_unhandled.js:*
+ throw new Error('One');
+ ^
+Error: One
+ at *promise_always_throw_unhandled.js:*:*
+ at new Promise (<anonymous>)
+ at Object.<anonymous> (*promise_always_throw_unhandled.js:*:*)
+ at *
+ at *
+ at *
+ at *
+ at *
+ at *
diff --git a/test/parallel/test-cli-node-options.js b/test/parallel/test-cli-node-options.js
index 5fd6c1f2c9..d3d1938bf8 100644
--- a/test/parallel/test-cli-node-options.js
+++ b/test/parallel/test-cli-node-options.js
@@ -34,6 +34,7 @@ expect('--trace-event-file-pattern {pid}-${rotation}.trace_events', 'B\n');
// eslint-disable-next-line no-template-curly-in-string
expect('--trace-event-file-pattern {pid}-${rotation}.trace_events ' +
'--trace-event-categories node.async_hooks', 'B\n');
+expect('--unhandled-rejections=none', 'B\n');
if (!common.isWindows) {
expect('--perf-basic-prof', 'B\n');
diff --git a/test/parallel/test-next-tick-errors.js b/test/parallel/test-next-tick-errors.js
index 51ed2524a0..bf142bb351 100644
--- a/test/parallel/test-next-tick-errors.js
+++ b/test/parallel/test-next-tick-errors.js
@@ -61,7 +61,9 @@ testNextTickWith('str');
testNextTickWith({});
testNextTickWith([]);
-process.on('uncaughtException', function() {
+process.on('uncaughtException', function(err, errorOrigin) {
+ assert.strictEqual(errorOrigin, 'uncaughtException');
+
if (!exceptionHandled) {
exceptionHandled = true;
order.push('B');
diff --git a/test/parallel/test-promise-unhandled-error.js b/test/parallel/test-promise-unhandled-error.js
new file mode 100644
index 0000000000..bb906fdcbe
--- /dev/null
+++ b/test/parallel/test-promise-unhandled-error.js
@@ -0,0 +1,54 @@
+// Flags: --unhandled-rejections=strict
+'use strict';
+
+const common = require('../common');
+const Countdown = require('../common/countdown');
+const assert = require('assert');
+
+common.disableCrashOnUnhandledRejection();
+
+// Verify that unhandled rejections always trigger uncaught exceptions instead
+// of triggering unhandled rejections.
+
+const err1 = new Error('One');
+const err2 = new Error(
+ '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 "null".'
+);
+err2.code = 'ERR_UNHANDLED_REJECTION';
+Object.defineProperty(err2, 'name', {
+ value: 'UnhandledPromiseRejection',
+ writable: true,
+ configurable: true
+});
+
+const errors = [err1, err2];
+const identical = [true, false];
+
+const ref = new Promise(() => {
+ throw err1;
+});
+// Explicitly reject `null`.
+Promise.reject(null);
+
+process.on('warning', common.mustNotCall('warning'));
+process.on('unhandledRejection', common.mustCall(2));
+process.on('rejectionHandled', common.mustNotCall('rejectionHandled'));
+process.on('exit', assert.strictEqual.bind(null, 0));
+
+const timer = setTimeout(() => console.log(ref), 1000);
+
+const counter = new Countdown(2, () => {
+ clearTimeout(timer);
+});
+
+process.on('uncaughtException', common.mustCall((err, origin) => {
+ counter.dec();
+ assert.strictEqual(origin, 'unhandledRejection', err);
+ const knownError = errors.shift();
+ assert.deepStrictEqual(err, knownError);
+ // Check if the errors are reference equal.
+ assert(identical.shift() ? err === knownError : err !== knownError);
+}, 2));
diff --git a/test/parallel/test-promise-unhandled-flag.js b/test/parallel/test-promise-unhandled-flag.js
new file mode 100644
index 0000000000..40a9030917
--- /dev/null
+++ b/test/parallel/test-promise-unhandled-flag.js
@@ -0,0 +1,16 @@
+'use strict';
+
+require('../common');
+const assert = require('assert');
+const cp = require('child_process');
+
+// Verify that a faulty environment variable throws on bootstrapping.
+// Therefore we do not need any special handling for the child process.
+const child = cp.spawnSync(
+ process.execPath,
+ ['--unhandled-rejections=foobar', __filename]
+);
+
+assert.strictEqual(child.stdout.toString(), '');
+assert(child.stderr.includes(
+ 'invalid value for --unhandled-rejections'), child.stderr);
diff --git a/test/parallel/test-promise-unhandled-silent-no-hook.js b/test/parallel/test-promise-unhandled-silent-no-hook.js
new file mode 100644
index 0000000000..db4f7a8471
--- /dev/null
+++ b/test/parallel/test-promise-unhandled-silent-no-hook.js
@@ -0,0 +1,22 @@
+// Flags: --unhandled-rejections=none
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+
+common.disableCrashOnUnhandledRejection();
+
+// Verify that ignoring unhandled rejection works fine and that no warning is
+// logged even though there is no unhandledRejection hook attached.
+
+new Promise(() => {
+ throw new Error('One');
+});
+
+Promise.reject('test');
+
+process.on('warning', common.mustNotCall('warning'));
+process.on('uncaughtException', common.mustNotCall('uncaughtException'));
+process.on('exit', assert.strictEqual.bind(null, 0));
+
+setTimeout(common.mustCall(), 2);
diff --git a/test/parallel/test-promise-unhandled-silent.js b/test/parallel/test-promise-unhandled-silent.js
new file mode 100644
index 0000000000..c9ccc0118e
--- /dev/null
+++ b/test/parallel/test-promise-unhandled-silent.js
@@ -0,0 +1,23 @@
+// Flags: --unhandled-rejections=none
+'use strict';
+
+const common = require('../common');
+
+common.disableCrashOnUnhandledRejection();
+
+// Verify that ignoring unhandled rejection works fine and that no warning is
+// logged.
+
+new Promise(() => {
+ throw new Error('One');
+});
+
+Promise.reject('test');
+
+process.on('warning', common.mustNotCall('warning'));
+process.on('uncaughtException', common.mustNotCall('uncaughtException'));
+process.on('rejectionHandled', common.mustNotCall('rejectionHandled'));
+
+process.on('unhandledRejection', common.mustCall(2));
+
+setTimeout(common.mustCall(), 2);
diff --git a/test/parallel/test-promise-unhandled-warn-no-hook.js b/test/parallel/test-promise-unhandled-warn-no-hook.js
new file mode 100644
index 0000000000..91784448ad
--- /dev/null
+++ b/test/parallel/test-promise-unhandled-warn-no-hook.js
@@ -0,0 +1,23 @@
+// Flags: --unhandled-rejections=warn
+'use strict';
+
+const common = require('../common');
+
+common.disableCrashOnUnhandledRejection();
+
+// Verify that ignoring unhandled rejection works fine and that no warning is
+// logged.
+
+new Promise(() => {
+ throw new Error('One');
+});
+
+Promise.reject('test');
+
+// Unhandled rejections trigger two warning per rejection. One is the rejection
+// reason and the other is a note where this warning is coming from.
+process.on('warning', common.mustCall(4));
+process.on('uncaughtException', common.mustNotCall('uncaughtException'));
+process.on('rejectionHandled', common.mustNotCall('rejectionHandled'));
+
+setTimeout(common.mustCall(), 2);
diff --git a/test/parallel/test-promise-unhandled-warn.js b/test/parallel/test-promise-unhandled-warn.js
new file mode 100644
index 0000000000..62ddc69d8c
--- /dev/null
+++ b/test/parallel/test-promise-unhandled-warn.js
@@ -0,0 +1,28 @@
+// Flags: --unhandled-rejections=warn
+'use strict';
+
+const common = require('../common');
+
+common.disableCrashOnUnhandledRejection();
+
+// Verify that ignoring unhandled rejection works fine and that no warning is
+// logged.
+
+new Promise(() => {
+ throw new Error('One');
+});
+
+Promise.reject('test');
+
+// Unhandled rejections trigger two warning per rejection. One is the rejection
+// reason and the other is a note where this warning is coming from.
+process.on('warning', common.mustCall(4));
+process.on('uncaughtException', common.mustNotCall('uncaughtException'));
+process.on('rejectionHandled', common.mustCall(2));
+
+process.on('unhandledRejection', (reason, promise) => {
+ // Handle promises but still warn!
+ promise.catch(() => {});
+});
+
+setTimeout(common.mustCall(), 2);
diff --git a/test/parallel/test-timers-immediate-queue-throw.js b/test/parallel/test-timers-immediate-queue-throw.js
index 9929b27ab2..477f0388f6 100644
--- a/test/parallel/test-timers-immediate-queue-throw.js
+++ b/test/parallel/test-timers-immediate-queue-throw.js
@@ -23,8 +23,11 @@ const errObj = {
message: 'setImmediate Err'
};
-process.once('uncaughtException', common.expectsError(errObj));
-process.once('uncaughtException', () => assert.strictEqual(stage, 0));
+process.once('uncaughtException', common.mustCall((err, errorOrigin) => {
+ assert.strictEqual(errorOrigin, 'uncaughtException');
+ assert.strictEqual(stage, 0);
+ common.expectsError(errObj)(err);
+}));
const d1 = domain.create();
d1.once('error', common.expectsError(errObj));
diff --git a/test/parallel/test-timers-unref-throw-then-ref.js b/test/parallel/test-timers-unref-throw-then-ref.js
index d3ae27e835..1dd5fdd0ad 100644
--- a/test/parallel/test-timers-unref-throw-then-ref.js
+++ b/test/parallel/test-timers-unref-throw-then-ref.js
@@ -2,8 +2,10 @@
const common = require('../common');
const assert = require('assert');
-process.once('uncaughtException', common.expectsError({
- message: 'Timeout Error'
+process.once('uncaughtException', common.mustCall((err) => {
+ common.expectsError({
+ message: 'Timeout Error'
+ })(err);
}));
let called = false;