An update on this: although the conversation somewhat fizzled here, in io.js 
(Node.js fork) something very similar is landing and I'm trying to guide it 
toward being reasonably compatible with browsers [1]. Additionally several 
promise libraries have implemented similar hooks (see [2] and links in the 
comments; also of interest is the analysis of how promise-using libraries use 
these hooks).

The community largely settled on unhandledRejection/rejectionHandled, instead 
of hijacking the error event like the original post below proposes. Which is 
not to say that we have to respect that in browsers, but it's a data point to 
consider.

One interesting question that came up is the exact timing of the 
unhandledRejection event. In my proto-spec at [3] I proposed queuing a separate 
notify-rejected task for each rejection, largely because it seemed the easiest 
thing to spec. We now have some experience from the field. The implementation 
at [1] considered a few approaches and cycled through implementing some subset 
of them to make a progressively-larger set of tests pass:

- Queue a single task
- Queue a separate task per rejection (as in proto-spec)
- Queue a microtask that occurs after all other microtasks
- Queue a task that occurs after all other tasks

Hopefully Petka, CC'ed, can correct me if I misstated these and fill in any 
details I missed.

In general I am in favor of pushing off notification as long as possible to 
give more time for the rejection to potentially become handled (and thus 
decrease false positives). From this perspective either separate task per 
rejection or after-all-others task seems good. I was hoping to get a web 
platform perspective on what sounds good and would be implementable? For 
example I think the after-all-others-task can be specced with a new task source 
that HTML mandates is drained after all others, right?

Anyway, I mostly just wanted to give people an update and show that we're 
prototyping this in io.js. Hopefully the interchange of ideas here can help 
push the progress in browsers too.


[1]: https://github.com/iojs/io.js/pull/758
[2]: https://gist.github.com/benjamingr/0237932cee84712951a2
[3]: 
https://gist.github.com/domenic/9b40029f59f29b822f3b#promise-error-handling-hooks-rough-spec-algorithm


-----Original Message-----
From: Domenic Denicola 
Sent: Friday, September 12, 2014 14:34
To: WHATWG
Subject: 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 :).

Reply via email to