A common desire in web programming is to log any uncaught exceptions back to the server. The typical method for doing this is
window.onerror = (message, url, line, column, error) => {
// log `error` back to the server
};
When programming asynchronously with promises, asynchronous exceptions are encapsulated as rejected promises. They can be caught and handled with promise.catch(err => ...)
, and propagate up through an "asynchronous call stack" (i.e. a promise chain) in a similar manner to synchronous errors.
However, for promises, there is no notion of the "top-level" of the promise chain at which the rejection is known to be unhandled. Promises are inherently temporal, and at any time code that has access to a given promise could handle the rejection it encapsulates. Thus, unlike with synchronous code, there is not an ever-growing list of unhandled exceptions: instead, there is a growing and shrinking list of currently-unhandled rejections.
For developers to be able to debug promises effectively, this live list of currently-unhandled rejections certainly needs to be exposed via developer tools, similar to how devtools exposes the ever-growing list of unhandled exceptions (via console output). However, developer tools are not sufficient to satisfy the telemetry use case, i.e. the use case which is currently handled via window.onerror
for synchronous code.
We propose that
window.onerror
be extended to handle the rejected-promise use case, notifying about any promises that, at the end of the task queue, contain rejections that are not yet handled; and- A new hook,
window.onrejectionhandled
, be added, to notify when (or if) such rejections eventually become handled.
In terms of developer experience, the result is that if a promise is rejected without any rejection handler present, and one is not attached by the end of the event loop turn, the resulting (message, url, line, column, error, promise)
tuple will hit window.onerror
. If the developer subsequently attaches a rejection handler to that promise, then the promise
object will be passed to any handlers for the rejectionhandled
event.
As usual, if one or both of these events is missing listeners, nothing will happen. (In this case, the developer likely does not want to do telemetry on errors, but instead will be availing themselves to the devtools.)
A robust error-reporting system would use rejectionhandled
events to cancel out earlier error
events, never displaying them to the person reading the error report.
We would extend ErrorEvent
and ErrorEventInit
with a promise
member. Similarly, we would extend the OnErrorEventHandlerNonNull
callback type to take as its last argument that same promise.
We would add a new event to the global, named rejectionhandled
, along with a RejectionHandledEvent
class that contains only a promise
member.
We would need to hook into rejecting promises and then
-ing promises, and track unhandled rejections. At the end of the task loop, if there are currently-unhandled rejections, we would fire the appropriate error
event. If a promise is then
-ed in such a way as to handle the rejection (either by the user directly, or by any internal spec mechanisms), and that promise had previously been reported as an unhandled rejection, we would need to fire the appropriate rejectionhandled
event. I can go into details on how to modify the promises spec to have these hooks, if desired, as well as how HTML would exploit them to maintain the appropriate list and report it at the end of the task queue.
The error
event and its idiosyncratic handler are not the best possible extension points. We may be better off with a separate unhandledrejection
event (or, more accurately and as popular libraries call it, possiblyunhandledrejection
). We could even unify on a single event class used for both, e.g. PromiseRejectionEvent
with members promise
and reason
. This improves clarity and reduces piling kludges on top of window.onerror
, but requires any existing telemetry code to upgrade to support the new event.