Promise Rejection Events
Elide implements the [HTML promise-rejection events][spec] required by the
[WinterCG Minimum Common API][wintercg]. Two events fire at globalThis to surface promise
rejections that escape userland handling:
unhandledrejection— dispatched after the microtask checkpoint completes for every promise rejected without a handler attached. Cancelable: callingpreventDefault()on the event suppresses the runtime's default action of writing the rejection tostderr.rejectionhandled— dispatched when a handler is attached to a previously-rejected promise afterunhandledrejectionalready fired for it. Non-cancelable.
[spec]: https://html.spec.whatwg.org/#unhandled-promise-rejections [wintercg]: https://min-common-api.proposal.wintercg.org/
PromiseRejectionEvent
new PromiseRejectionEvent(type, eventInitDict)
// eventInitDict: { promise: Promise<any> /* required */, reason?: any,
// bubbles?: boolean = false, cancelable?: boolean = false,
// composed?: boolean = false }Event: event instanceof Event is true, and every Event member works on a
PromiseRejectionEvent. Adds two readonly attributes:
| Member | Type | Notes |
|---|---|---|
promise | Promise | Readonly; the rejected promise, taken from the init dictionary or the runtime tracker. |
reason | any | Readonly; the rejection reason (the value passed to Promise.reject, the value of a thrown error inside async code, etc.). For rejectionhandled events the runtime reads this from the promise's stored result. |
promise member is required in the init dictionary — calling new PromiseRejectionEvent("foo")
or new PromiseRejectionEvent("foo", {}) raises TypeError, matching the WebIDL required marker.
Plain-call PromiseRejectionEvent(...) (without new) raises TypeError per the standard
WebIDL rule for interfaces with constructors.
Event delivery on globalThis
globalThis exposes the standard EventTarget triad — addEventListener, removeEventListener,
dispatchEvent — wired up at realm-init time. Listener registration is identical to a regular
EventTarget:
globalThis.addEventListener("unhandledrejection", (event) => {
console.warn("unhandled rejection:", event.reason);
event.preventDefault(); // suppress the default stderr line
});
globalThis.addEventListener("rejectionhandled", (event) => {
console.info("late handler attached for:", event.reason);
});The two events also surface as event-handler properties:
globalThis.onunhandledrejection = (e) => { /* ... */ };
globalThis.onrejectionhandled = (e) => { /* ... */ };These follow the standard HTML EventHandler reflection algorithm — assigning a function replaces
the previous handler (the previous one is removeEventListener'd first), assigning null
or any non-callable value clears the slot. Both addEventListener-style and on-property-style
listeners receive the same trusted PromiseRejectionEvent instance and may interleave.
When the events fire
The runtime tracks promise-rejection state through three transitions, mirroring the HTML "notify about rejected promises" algorithm:
1. Promise rejects with no handler attached — buffered into a per-realm "about-to-be-notified"
list. No event yet; a handler attached later in the same microtask drain can still suppress it.
2. Microtask drain completes — every entry still in the list dispatches unhandledrejection
on globalThis. If the listener calls preventDefault() the runtime skips the default
stderr-log action; the promise still moves to the "outstanding" set so a later
handler-attach can fire rejectionhandled for it.
3. Handler attaches after unhandledrejection already fired — fires rejectionhandled on
globalThis for that promise.
Promises whose handlers attach during the same microtask cycle (before the drain checkpoint) never see either event — the rejection is considered handled in time.
isTrusted
Events fired by the runtime tracker have isTrusted === true — the spec-mandated bit that
distinguishes implementation-fired events from those constructed in user code. Events created
via new PromiseRejectionEvent(...) and dispatched manually have isTrusted === false,
matching dispatchEvent's behaviour for every other Event subclass.
Notes on the implementation
globalThisis not (yet) itself the canonicalEventTargetinstance —addEventListenerand friends delegate to a hidden per-realm event target. Listeners observeevent.targetset to that hidden target, notglobalThis. ManualglobalThis.dispatchEvent(event)calls land on the same hidden target, so userland code that registers and dispatches throughglobalThisis internally consistent.- The runtime tracker is installed on the JS context via graal-js's
JSContext.setPromiseRejectionTrackerhook, so the buffering and microtask-boundary timing are driven by the same code path graal-js uses for promise reactions — there's no separate scheduler. - The default stderr line is
Uncaught (in promise)— best-effort string of the reason value viaJSRuntime.safeToString, which avoids re-entering user code while logging.