diff --git a/cli/tests/integration/node_unit_tests.rs b/cli/tests/integration/node_unit_tests.rs index 2cde515528..7c524eb8be 100644 --- a/cli/tests/integration/node_unit_tests.rs +++ b/cli/tests/integration/node_unit_tests.rs @@ -5,6 +5,7 @@ use std::process::Stdio; use std::time::Duration; use std::time::Instant; use test_util as util; +use util::env_vars_for_npm_tests; util::unit_test_factory!( node_unit_test, @@ -157,3 +158,20 @@ fn node_unit_test(test: String) { assert!(status.success()); } + +// Regression test for https://github.com/denoland/deno/issues/16928 +itest!(unhandled_rejection_web { + args: "run -A node/unhandled_rejection_web.ts", + output: "node/unhandled_rejection_web.ts.out", + envs: env_vars_for_npm_tests(), + http_server: true, +}); + +// Ensure that Web `onunhandledrejection` is fired before +// Node's `process.on('unhandledRejection')`. +itest!(unhandled_rejection_web_process { + args: "run -A node/unhandled_rejection_web_process.ts", + output: "node/unhandled_rejection_web_process.ts.out", + envs: env_vars_for_npm_tests(), + http_server: true, +}); diff --git a/cli/tests/testdata/node/unhandled_rejection_web.ts b/cli/tests/testdata/node/unhandled_rejection_web.ts new file mode 100644 index 0000000000..396c58c2a1 --- /dev/null +++ b/cli/tests/testdata/node/unhandled_rejection_web.ts @@ -0,0 +1,17 @@ +import chalk from "npm:chalk"; + +console.log(chalk.red("Hello world!")); + +globalThis.addEventListener("unhandledrejection", (e) => { + console.log("Handled the promise rejection"); + e.preventDefault(); +}); + +// deno-lint-ignore require-await +(async () => { + throw new Error("boom!"); +})(); + +setTimeout(() => { + console.log("Success"); +}, 1000); diff --git a/cli/tests/testdata/node/unhandled_rejection_web.ts.out b/cli/tests/testdata/node/unhandled_rejection_web.ts.out new file mode 100644 index 0000000000..19db7f90e5 --- /dev/null +++ b/cli/tests/testdata/node/unhandled_rejection_web.ts.out @@ -0,0 +1,4 @@ +[WILDCARD] +Hello world! +Handled the promise rejection +Success diff --git a/cli/tests/testdata/node/unhandled_rejection_web_process.ts b/cli/tests/testdata/node/unhandled_rejection_web_process.ts new file mode 100644 index 0000000000..2aaacfbff7 --- /dev/null +++ b/cli/tests/testdata/node/unhandled_rejection_web_process.ts @@ -0,0 +1,21 @@ +import chalk from "npm:chalk"; +import process from "node:process"; + +console.log(chalk.red("Hello world!")); + +process.on("unhandledRejection", (_e) => { + console.log('process.on("unhandledRejection");'); +}); + +globalThis.addEventListener("unhandledrejection", (_e) => { + console.log('globalThis.addEventListener("unhandledrejection");'); +}); + +// deno-lint-ignore require-await +(async () => { + throw new Error("boom!"); +})(); + +setTimeout(() => { + console.log("Success"); +}, 1000); diff --git a/cli/tests/testdata/node/unhandled_rejection_web_process.ts.out b/cli/tests/testdata/node/unhandled_rejection_web_process.ts.out new file mode 100644 index 0000000000..ea307474a8 --- /dev/null +++ b/cli/tests/testdata/node/unhandled_rejection_web_process.ts.out @@ -0,0 +1,5 @@ +[WILDCARD] +Hello world! +globalThis.addEventListener("unhandledrejection"); +process.on("unhandledRejection"); +Success diff --git a/ext/node/polyfills/process.ts b/ext/node/polyfills/process.ts index d2f220734f..2dc10d7b15 100644 --- a/ext/node/polyfills/process.ts +++ b/ext/node/polyfills/process.ts @@ -716,9 +716,9 @@ internals.__bootstrapNodeProcess = function ( core.setMacrotaskCallback(runNextTicks); enableNextTick(); - // TODO(bartlomieju): this is buggy, see https://github.com/denoland/deno/issues/16928 - // We should use a specialized API in 99_main.js instead - globalThis.addEventListener("unhandledrejection", (event) => { + // Install special "unhandledrejection" handler, that will be called + // last. + internals.nodeProcessUnhandledRejectionCallback = (event) => { if (process.listenerCount("unhandledRejection") === 0) { // The Node.js default behavior is to raise an uncaught exception if // an unhandled rejection occurs and there are no unhandledRejection @@ -734,7 +734,7 @@ internals.__bootstrapNodeProcess = function ( event.preventDefault(); process.emit("unhandledRejection", event.reason, event.promise); - }); + }; globalThis.addEventListener("error", (event) => { if (process.listenerCount("uncaughtException") > 0) { diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js index f0c63df744..91d34272f9 100644 --- a/runtime/js/99_main.js +++ b/runtime/js/99_main.js @@ -346,7 +346,8 @@ function promiseRejectCallback(type, promise, reason) { } return !!globalThis_.onunhandledrejection || - event.listenerCount(globalThis_, "unhandledrejection") > 0; + event.listenerCount(globalThis_, "unhandledrejection") > 0 || + typeof internals.nodeProcessUnhandledRejectionCallback !== "undefined"; } function promiseRejectMacrotaskCallback() { @@ -383,6 +384,15 @@ function promiseRejectMacrotaskCallback() { globalThis_.dispatchEvent(rejectionEvent); globalThis_.removeEventListener("error", errorEventCb); + // If event was not yet prevented, try handing it off to Node compat layer + // (if it was initialized) + if ( + !rejectionEvent.defaultPrevented && + typeof internals.nodeProcessUnhandledRejectionCallback !== "undefined" + ) { + internals.nodeProcessUnhandledRejectionCallback(rejectionEvent); + } + // If event was not prevented (or "unhandledrejection" listeners didn't // throw) we will let Rust side handle it. if (rejectionEvent.defaultPrevented) {