feat: add "unhandledrejection" event support (#12994) (#15080)

Relanding #12994 

This commit adds support for "unhandledrejection" event.

This event will trigger event listeners registered using:

"globalThis.addEventListener("unhandledrejection")
"globalThis.onunhandledrejection"
This is done by registering a default handler using
"Deno.core.setPromiseRejectCallback" that allows to
handle rejected promises in JavaScript instead of Rust.

This commit will make it possible to polyfill
"process.on("unhandledRejection")" in the Node compat
layer.

Co-authored-by: Colin Ihrig <cjihrig@gmail.com>
This commit is contained in:
Bartek Iwańczuk 2022-07-14 22:39:20 +02:00 committed by GitHub
parent 48a7312f38
commit 1a7259b04b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 213 additions and 10 deletions

View file

@ -400,6 +400,17 @@ declare class ErrorEvent extends Event {
constructor(type: string, eventInitDict?: ErrorEventInit);
}
interface PromiseRejectionEventInit extends EventInit {
promise: Promise<any>;
reason?: any;
}
declare class PromiseRejectionEvent extends Event {
readonly promise: Promise<any>;
readonly reason: any;
constructor(type: string, eventInitDict?: PromiseRejectionEventInit);
}
interface AbstractWorkerEventMap {
"error": ErrorEvent;
}

View file

@ -2783,3 +2783,8 @@ itest!(followup_dyn_import_resolved {
args: "run --unstable --allow-read followup_dyn_import_resolves/main.ts",
output: "followup_dyn_import_resolves/main.ts.out",
});
itest!(unhandled_rejection {
args: "run --allow-read unhandled_rejection.js",
output: "unhandled_rejection.js.out",
});

View file

@ -0,0 +1,11 @@
globalThis.addEventListener("unhandledrejection", (e) => {
console.log("unhandled rejection at:", e.promise, "reason:", e.reason);
e.preventDefault();
});
function Foo() {
this.bar = Promise.reject(new Error("bar not available"));
}
new Foo();
Promise.reject();

View file

@ -0,0 +1,8 @@
unhandled rejection at: Promise {
<rejected> Error: bar not available
at new Foo (file:///[WILDCARD]/testdata/unhandled_rejection.js:7:29)
at file:///[WILDCARD]/testdata/unhandled_rejection.js:10:1
} reason: Error: bar not available
at new Foo (file:///[WILDCARD]/testdata/unhandled_rejection.js:7:29)
at file:///[WILDCARD]/testdata/unhandled_rejection.js:10:1
unhandled rejection at: Promise { <rejected> undefined } reason: undefined

View file

@ -268,6 +268,10 @@
terminate: opSync.bind(null, "op_terminate"),
opNames: opSync.bind(null, "op_op_names"),
eventLoopHasMoreWork: opSync.bind(null, "op_event_loop_has_more_work"),
setPromiseRejectCallback: opSync.bind(
null,
"op_set_promise_reject_callback",
),
});
ObjectAssign(globalThis.__bootstrap, { core });

View file

@ -282,14 +282,6 @@ pub extern "C" fn promise_reject_callback(message: v8::PromiseRejectMessage) {
let state_rc = JsRuntime::state(scope);
let mut state = state_rc.borrow_mut();
// Node compat: perform synchronous process.emit("unhandledRejection").
//
// Note the callback follows the (type, promise, reason) signature of Node's
// internal promiseRejectHandler from lib/internal/process/promises.js, not
// the (promise, reason) signature of the "unhandledRejection" event listener.
//
// Short-circuits Deno's regular unhandled rejection logic because that's
// a) asynchronous, and b) always terminates.
if let Some(js_promise_reject_cb) = state.js_promise_reject_cb.clone() {
let js_uncaught_exception_cb = state.js_uncaught_exception_cb.clone();
@ -331,6 +323,7 @@ pub extern "C" fn promise_reject_callback(message: v8::PromiseRejectMessage) {
}
if tc_scope.has_caught() {
// TODO(bartlomieju): ensure that TODO provided below is still valid.
// If we get here, an exception was thrown by the unhandledRejection
// handler and there is ether no uncaughtException handler or the
// handler threw an exception of its own.
@ -348,7 +341,6 @@ pub extern "C" fn promise_reject_callback(message: v8::PromiseRejectMessage) {
} else {
let promise = message.get_promise();
let promise_global = v8::Global::new(scope, promise);
match message.get_event() {
PromiseRejectWithNoHandler => {
let error = message.get_value().unwrap();

View file

@ -48,6 +48,9 @@ pub(crate) fn init_builtins_v8() -> Vec<OpDecl> {
op_apply_source_map::decl(),
op_set_format_exception_callback::decl(),
op_event_loop_has_more_work::decl(),
op_store_pending_promise_exception::decl(),
op_remove_pending_promise_exception::decl(),
op_has_pending_promise_exception::decl(),
]
}
@ -792,3 +795,48 @@ fn op_set_format_exception_callback<'a>(
fn op_event_loop_has_more_work(scope: &mut v8::HandleScope) -> bool {
JsRuntime::event_loop_pending_state(scope).is_pending()
}
#[op(v8)]
fn op_store_pending_promise_exception<'a>(
scope: &mut v8::HandleScope<'a>,
promise: serde_v8::Value<'a>,
reason: serde_v8::Value<'a>,
) {
let state_rc = JsRuntime::state(scope);
let mut state = state_rc.borrow_mut();
let promise_value =
v8::Local::<v8::Promise>::try_from(promise.v8_value).unwrap();
let promise_global = v8::Global::new(scope, promise_value);
let error_global = v8::Global::new(scope, reason.v8_value);
state
.pending_promise_exceptions
.insert(promise_global, error_global);
}
#[op(v8)]
fn op_remove_pending_promise_exception<'a>(
scope: &mut v8::HandleScope<'a>,
promise: serde_v8::Value<'a>,
) {
let state_rc = JsRuntime::state(scope);
let mut state = state_rc.borrow_mut();
let promise_value =
v8::Local::<v8::Promise>::try_from(promise.v8_value).unwrap();
let promise_global = v8::Global::new(scope, promise_value);
state.pending_promise_exceptions.remove(&promise_global);
}
#[op(v8)]
fn op_has_pending_promise_exception<'a>(
scope: &mut v8::HandleScope<'a>,
promise: serde_v8::Value<'a>,
) -> bool {
let state_rc = JsRuntime::state(scope);
let state = state_rc.borrow();
let promise_value =
v8::Local::<v8::Promise>::try_from(promise.v8_value).unwrap();
let promise_global = v8::Global::new(scope, promise_value);
state
.pending_promise_exceptions
.contains_key(&promise_global)
}

View file

@ -1278,6 +1278,58 @@
[SymbolToStringTag] = "ProgressEvent";
}
class PromiseRejectionEvent extends Event {
#promise = null;
#reason = null;
get promise() {
return this.#promise;
}
get reason() {
return this.#reason;
}
constructor(
type,
{
bubbles,
cancelable,
composed,
promise,
reason,
} = {},
) {
super(type, {
bubbles: bubbles,
cancelable: cancelable,
composed: composed,
});
this.#promise = promise;
this.#reason = reason;
}
[SymbolFor("Deno.privateCustomInspect")](inspect) {
return inspect(consoleInternal.createFilteredInspectProxy({
object: this,
evaluate: this instanceof PromiseRejectionEvent,
keys: [
...EVENT_PROPS,
"promise",
"reason",
],
}));
}
// TODO(lucacasonato): remove when this interface is spec aligned
[SymbolToStringTag] = "PromiseRejectionEvent";
}
defineEnumerableProps(PromiseRejectionEvent, [
"promise",
"reason",
]);
const _eventHandlers = Symbol("eventHandlers");
function makeWrappedHandler(handler, isSpecialErrorEventHandler) {
@ -1426,6 +1478,7 @@
window.MessageEvent = MessageEvent;
window.CustomEvent = CustomEvent;
window.ProgressEvent = ProgressEvent;
window.PromiseRejectionEvent = PromiseRejectionEvent;
window.dispatchEvent = EventTarget.prototype.dispatchEvent;
window.addEventListener = EventTarget.prototype.addEventListener;
window.removeEventListener = EventTarget.prototype.removeEventListener;

View file

@ -11,6 +11,10 @@ delete Intl.v8BreakIterator;
((window) => {
const core = Deno.core;
const {
ArrayPrototypeIndexOf,
ArrayPrototypePush,
ArrayPrototypeShift,
ArrayPrototypeSplice,
ArrayPrototypeMap,
DateNow,
Error,
@ -27,7 +31,11 @@ delete Intl.v8BreakIterator;
SymbolFor,
SymbolIterator,
PromisePrototypeThen,
SafeWeakMap,
TypeError,
WeakMapPrototypeDelete,
WeakMapPrototypeGet,
WeakMapPrototypeSet,
} = window.__bootstrap.primordials;
const util = window.__bootstrap.util;
const eventTarget = window.__bootstrap.eventTarget;
@ -233,6 +241,7 @@ delete Intl.v8BreakIterator;
function runtimeStart(runtimeOptions, source) {
core.setMacrotaskCallback(timers.handleTimerMacrotask);
core.setMacrotaskCallback(promiseRejectMacrotaskCallback);
core.setWasmStreamingCallback(fetch.handleWasmStreaming);
core.opSync("op_set_format_exception_callback", formatException);
version.setVersions(
@ -411,6 +420,7 @@ delete Intl.v8BreakIterator;
PerformanceEntry: util.nonEnumerable(performance.PerformanceEntry),
PerformanceMark: util.nonEnumerable(performance.PerformanceMark),
PerformanceMeasure: util.nonEnumerable(performance.PerformanceMeasure),
PromiseRejectionEvent: util.nonEnumerable(PromiseRejectionEvent),
ProgressEvent: util.nonEnumerable(ProgressEvent),
ReadableStream: util.nonEnumerable(streams.ReadableStream),
ReadableStreamDefaultReader: util.nonEnumerable(
@ -553,6 +563,60 @@ delete Intl.v8BreakIterator;
postMessage: util.writable(postMessage),
};
const pendingRejections = [];
const pendingRejectionsReasons = new SafeWeakMap();
function promiseRejectCallback(type, promise, reason) {
switch (type) {
case 0: {
core.opSync("op_store_pending_promise_exception", promise, reason);
ArrayPrototypePush(pendingRejections, promise);
WeakMapPrototypeSet(pendingRejectionsReasons, promise, reason);
break;
}
case 1: {
core.opSync("op_remove_pending_promise_exception", promise);
const index = ArrayPrototypeIndexOf(pendingRejections, promise);
if (index > -1) {
ArrayPrototypeSplice(pendingRejections, index, 1);
WeakMapPrototypeDelete(pendingRejectionsReasons, promise);
}
break;
}
default:
return;
}
}
function promiseRejectMacrotaskCallback() {
while (pendingRejections.length > 0) {
const promise = ArrayPrototypeShift(pendingRejections);
const hasPendingException = core.opSync(
"op_has_pending_promise_exception",
promise,
);
const reason = WeakMapPrototypeGet(pendingRejectionsReasons, promise);
WeakMapPrototypeDelete(pendingRejectionsReasons, promise);
if (!hasPendingException) {
return;
}
const event = new PromiseRejectionEvent("unhandledrejection", {
cancelable: true,
promise,
reason,
});
globalThis.dispatchEvent(event);
// If event was not prevented we will let Rust side handle it.
if (event.defaultPrevented) {
core.opSync("op_remove_pending_promise_exception", promise);
}
}
return true;
}
let hasBootstrapped = false;
function bootstrapMainRuntime(runtimeOptions) {
@ -585,6 +649,10 @@ delete Intl.v8BreakIterator;
defineEventHandler(window, "load");
defineEventHandler(window, "beforeunload");
defineEventHandler(window, "unload");
defineEventHandler(window, "unhandledrejection");
core.setPromiseRejectCallback(promiseRejectCallback);
const isUnloadDispatched = SymbolFor("isUnloadDispatched");
// Stores the flag for checking whether unload is dispatched or not.
// This prevents the recursive dispatches of unload events.
@ -685,6 +753,10 @@ delete Intl.v8BreakIterator;
defineEventHandler(self, "message");
defineEventHandler(self, "error", undefined, true);
defineEventHandler(self, "unhandledrejection");
core.setPromiseRejectCallback(promiseRejectCallback);
// `Deno.exit()` is an alias to `self.close()`. Setting and exit
// code using an op in worker context is a no-op.
os.setExitHandler((_exitCode) => {

View file

@ -4360,7 +4360,6 @@
"The CanvasPath interface object should be exposed.",
"The TextMetrics interface object should be exposed.",
"The Path2D interface object should be exposed.",
"The PromiseRejectionEvent interface object should be exposed.",
"The EventSource interface object should be exposed.",
"The XMLHttpRequestEventTarget interface object should be exposed.",
"The XMLHttpRequestUpload interface object should be exposed.",