- From: Domenic Denicola <d@domenic.me>
- Date: Tue, 10 Feb 2015 01:45:26 +0000
- To: WHATWG <whatwg@whatwg.org>
- Cc: Petka Antonov <petka.antonov@gmail.com>
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 :).
Received on Tuesday, 10 February 2015 01:45:55 UTC