feat: "rejectionhandled" Web event and "rejectionHandled" Node event (#21875)

This commit adds support for "rejectionhandled" Web Event and
"rejectionHandled" Node event.

```js
import process from "node:process";

process.on("rejectionHandled", (promise) => {
  console.log("rejectionHandled", reason, promise);
});

window.addEventListener("rejectionhandled", (event) => {
  console.log("rejectionhandled", event.reason, event.promise);
});
```

---------

Co-authored-by: Matt Mastracci <matthew@mastracci.com>
This commit is contained in:
Bartek Iwańczuk 2024-01-12 23:10:42 +01:00 committed by GitHub
parent 288774c5ed
commit 7471587d29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 106 additions and 1 deletions

View file

@ -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,
});

View file

@ -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",

View file

@ -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);

View file

@ -0,0 +1,7 @@
[WILDCARD]
Hello world!
globalThis.addEventListener("unhandledrejection");
Added catch handler to the promise
Web rejectionhandled
Node rejectionHandled
Success

View file

@ -0,0 +1,5 @@
[WILDCARD]
unhandledrejection 1 Promise { <rejected> 1 }
Added catch handler to the promise
rejectionhandled 1 Promise { <rejected> 1 }
Success

View file

@ -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);

View file

@ -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;

View file

@ -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 {

View file

@ -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