[whatwg] An API for unhandled promise rejections

## Problem

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.

## Proposed Solution

We propose that

1. `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
2. A new hook, `window.onrejectionhandled`, be added, to notify when (or if) such rejections eventually become handled.

By "at the end of the task queue" I mean that upon a promise being rejected with no handlers, we would queue a task to fire an error event; however if a handler is then attached to a promise in the meantime, a flag would be set so that when the task executes nothing actually happens.

### Developer Experience

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.

### Specification Details

We would extend [`ErrorEvent`](http://www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#the-errorevent-interface) and `ErrorEventInit` with a `promise` member. Similarly, we would extend the  [`OnErrorEventHandlerNonNull`](http://www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#onerroreventhandlernonnull) callback type to take as its last argument that same promise. In both cases, the promise would be `undefined` for synchronous errors.

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:

* When a promise is rejected, if it has no handlers, we would queue a task to potentially-fire-an-error.
* When a promise is `then`'d (either by user code or by the spec's chaining mechanisms) but the rejection has not yet been reported, we would set a flag saying "don't fire that error after all."
* When the task is executed, if that flag is still unset, we would then fire the appropriate `error` event.
* If a promise is `then`-ed in such a way as to handle the rejection, but 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. I can also help with the spec work here, on both the ES side and the HTML side, if desired.

### Potential Variants

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](https://github.com/petkaantonov/bluebird/#error-handling) 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.

I personally think this is a better solution, both because it has less kludges and because I can see server telemetry tools that aren't upgraded to recognize the new duality becoming overwhelmed with useless `error` events that are later canceled by `unhandledrejection` events they are unaware of. That is, if you try to plug asynchronous errors into your existing telemetry systems, you will be pulling your hair out over spurious, and sometimes hard-to-reproduce, errors in your logs. But other members of the Chrome team feel strongly about re-using onerror and I am happy to let this play out in the real world.

Note that we are proposing this for the web, and not for ES, because the web has `window.onerror` (not to mention an event system) already. A more generic unhandled-rejection-tracking mechanism for all ES environments might be something like an `Object.observe`able `Promise.unhandledRejections` array, but that discussion can be left for another time.

## Implementer Interest

Chrome is interested in implementing this ASAP. I'm broaching the idea for the first time publicly to hopefully get other implementer interest or at least rough consensus :).

Received on Friday, 12 September 2014 18:35:10 UTC