diff --git a/cli/tests/integration/node_unit_tests.rs b/cli/tests/integration/node_unit_tests.rs index 273066b093..351bf1eecd 100644 --- a/cli/tests/integration/node_unit_tests.rs +++ b/cli/tests/integration/node_unit_tests.rs @@ -193,3 +193,12 @@ itest!(unhandled_rejection_web_process { envs: env_vars_for_npm_tests(), http_server: true, }); + +// Ensure that Web `onrejectionhandled` is fired before +// Node's `process.on('rejectionHandled')`. +itest!(rejection_handled_web_process { + args: "run -A node/rejection_handled_web_process.ts", + output: "node/rejection_handled_web_process.ts.out", + envs: env_vars_for_npm_tests(), + http_server: true, +}); diff --git a/cli/tests/integration/run_tests.rs b/cli/tests/integration/run_tests.rs index 2a349a5f26..999dc11778 100644 --- a/cli/tests/integration/run_tests.rs +++ b/cli/tests/integration/run_tests.rs @@ -3672,6 +3672,11 @@ itest!(unhandled_rejection_dynamic_import2 { output: "run/unhandled_rejection_dynamic_import2/main.ts.out", }); +itest!(rejection_handled { + args: "run --check run/rejection_handled.ts", + output: "run/rejection_handled.out", +}); + itest!(nested_error { args: "run run/nested_error/main.ts", output: "run/nested_error/main.ts.out", diff --git a/cli/tests/testdata/node/rejection_handled_web_process.ts b/cli/tests/testdata/node/rejection_handled_web_process.ts new file mode 100644 index 0000000000..00d943feb9 --- /dev/null +++ b/cli/tests/testdata/node/rejection_handled_web_process.ts @@ -0,0 +1,26 @@ +import chalk from "npm:chalk"; +import process from "node:process"; + +console.log(chalk.red("Hello world!")); + +globalThis.addEventListener("unhandledrejection", (e) => { + console.log('globalThis.addEventListener("unhandledrejection");'); + e.preventDefault(); +}); + +globalThis.addEventListener("rejectionhandled", (_) => { + console.log("Web rejectionhandled"); +}); + +process.on("rejectionHandled", (_) => { + console.log("Node rejectionHandled"); +}); + +const a = Promise.reject(1); +setTimeout(() => { + a.catch(() => console.log("Added catch handler to the promise")); +}, 10); + +setTimeout(() => { + console.log("Success"); +}, 50); diff --git a/cli/tests/testdata/node/rejection_handled_web_process.ts.out b/cli/tests/testdata/node/rejection_handled_web_process.ts.out new file mode 100644 index 0000000000..3a4e2ac234 --- /dev/null +++ b/cli/tests/testdata/node/rejection_handled_web_process.ts.out @@ -0,0 +1,7 @@ +[WILDCARD] +Hello world! +globalThis.addEventListener("unhandledrejection"); +Added catch handler to the promise +Web rejectionhandled +Node rejectionHandled +Success diff --git a/cli/tests/testdata/run/rejection_handled.out b/cli/tests/testdata/run/rejection_handled.out new file mode 100644 index 0000000000..5c06fcd2b3 --- /dev/null +++ b/cli/tests/testdata/run/rejection_handled.out @@ -0,0 +1,5 @@ +[WILDCARD] +unhandledrejection 1 Promise { <rejected> 1 } +Added catch handler to the promise +rejectionhandled 1 Promise { <rejected> 1 } +Success diff --git a/cli/tests/testdata/run/rejection_handled.ts b/cli/tests/testdata/run/rejection_handled.ts new file mode 100644 index 0000000000..f058ff9665 --- /dev/null +++ b/cli/tests/testdata/run/rejection_handled.ts @@ -0,0 +1,17 @@ +window.addEventListener("unhandledrejection", (event) => { + console.log("unhandledrejection", event.reason, event.promise); + event.preventDefault(); +}); + +window.addEventListener("rejectionhandled", (event) => { + console.log("rejectionhandled", event.reason, event.promise); +}); + +const a = Promise.reject(1); +setTimeout(async () => { + a.catch(() => console.log("Added catch handler to the promise")); +}, 10); + +setTimeout(() => { + console.log("Success"); +}, 50); diff --git a/cli/tsc/dts/lib.deno.window.d.ts b/cli/tsc/dts/lib.deno.window.d.ts index c518c53560..eaab7c3c23 100644 --- a/cli/tsc/dts/lib.deno.window.d.ts +++ b/cli/tsc/dts/lib.deno.window.d.ts @@ -12,6 +12,7 @@ declare interface WindowEventMap { "error": ErrorEvent; "unhandledrejection": PromiseRejectionEvent; + "rejectionhandled": PromiseRejectionEvent; } /** @category Web APIs */ @@ -25,6 +26,9 @@ declare interface Window extends EventTarget { onunhandledrejection: | ((this: Window, ev: PromiseRejectionEvent) => any) | null; + onrejectionhandled: + | ((this: Window, ev: PromiseRejectionEvent) => any) + | null; close: () => void; readonly closed: boolean; alert: (message?: string) => void; diff --git a/ext/node/polyfills/process.ts b/ext/node/polyfills/process.ts index 3d5009b90b..1edcccc009 100644 --- a/ext/node/polyfills/process.ts +++ b/ext/node/polyfills/process.ts @@ -75,7 +75,6 @@ import { buildAllowedFlags } from "ext:deno_node/internal/process/per_thread.mjs const notImplementedEvents = [ "multipleResolves", - "rejectionHandled", "worker", ]; @@ -746,6 +745,7 @@ export const removeListener = process.removeListener; export const removeAllListeners = process.removeAllListeners; let unhandledRejectionListenerCount = 0; +let rejectionHandledListenerCount = 0; let uncaughtExceptionListenerCount = 0; let beforeExitListenerCount = 0; let exitListenerCount = 0; @@ -755,6 +755,9 @@ process.on("newListener", (event: string) => { case "unhandledRejection": unhandledRejectionListenerCount++; break; + case "rejectionHandled": + rejectionHandledListenerCount++; + break; case "uncaughtException": uncaughtExceptionListenerCount++; break; @@ -775,6 +778,9 @@ process.on("removeListener", (event: string) => { case "unhandledRejection": unhandledRejectionListenerCount--; break; + case "rejectionHandled": + rejectionHandledListenerCount--; + break; case "uncaughtException": uncaughtExceptionListenerCount--; break; @@ -837,6 +843,16 @@ function synchronizeListeners() { internals.nodeProcessUnhandledRejectionCallback = undefined; } + // Install special "handledrejection" handler, that will be called + // last. + if (rejectionHandledListenerCount > 0) { + internals.nodeProcessRejectionHandledCallback = (event) => { + process.emit("rejectionHandled", event.reason, event.promise); + }; + } else { + internals.nodeProcessRejectionHandledCallback = undefined; + } + if (uncaughtExceptionListenerCount > 0) { globalThis.addEventListener("error", processOnError); } else { diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js index 4644d2d08c..6c5ca3b590 100644 --- a/runtime/js/99_main.js +++ b/runtime/js/99_main.js @@ -344,6 +344,8 @@ function runtimeStart( } core.setUnhandledPromiseRejectionHandler(processUnhandledPromiseRejection); +core.setHandledPromiseRejectionHandler(processRejectionHandled); + // Notification that the core received an unhandled promise rejection that is about to // terminate the runtime. If we can handle it, attempt to do so. function processUnhandledPromiseRejection(promise, reason) { @@ -377,6 +379,20 @@ function processUnhandledPromiseRejection(promise, reason) { return false; } +function processRejectionHandled(promise, reason) { + const rejectionHandledEvent = new event.PromiseRejectionEvent( + "rejectionhandled", + { promise, reason }, + ); + + // Note that the handler may throw, causing a recursive "error" event + globalThis_.dispatchEvent(rejectionHandledEvent); + + if (typeof internals.nodeProcessRejectionHandledCallback !== "undefined") { + internals.nodeProcessRejectionHandledCallback(rejectionHandledEvent); + } +} + let hasBootstrapped = false; // Delete the `console` object that V8 automaticaly adds onto the global wrapper // object on context creation. We don't want this console object to shadow the