Skip to content

Instantly share code, notes, and snippets.

@domenic
Last active October 31, 2019 02:53
Show Gist options
  • Save domenic/9b40029f59f29b822f3b to your computer and use it in GitHub Desktop.
Save domenic/9b40029f59f29b822f3b to your computer and use it in GitHub Desktop.
Proposal for promise error handling hooks

Promise Error Handling Hooks

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.

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 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.

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 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.

Promise Error Handling Hooks: Rough Spec Algorithm

ECMAScript/V8 algorithm

General modifications

All promises need to get a unique promise ID of some sort, e.g. a string or number. These can be created lazily if that is easier.

All promises get a "rejection status" field, which is one of: "nothing"; "notified of rejection"; ".then'd"

Modification to PromiseReject

If a promise is rejected (which currently always goes through PromiseReject, both in the spec and in V8), and its rejection status is not ".then'd", send a message to the host environment consisting of "potentially unhandled rejection: (the promise ID, the rejection reason)", and set its rejection status to "notified of rejection."

Modification to Promise.prototype.then

If the promise's rejection status is "notified of rejection," send a message to the host environment consisting of "rejection handled: (the promise ID)".

Set the promise's rejection status to ".then'd".

Host environment algorithm

The host environment has a list of outstanding rejected promise IDs and about-to-be-notified rejected promise IDs.

Receive "potentially unhandled rejection" messages, and:

  • Add the given promise ID to the list of about-to-be-notified rejected promise IDs.
  • Queue a notify-rejected task, with parameters promise ID and rejection reason.

Receive "rejection handled" messages, and:

  • Remove the given promise ID from the list of about-to-be-notified rejected promise IDs, if present.
  • If the given promise ID is in the list of outstanding rejected promise IDs, notify the user (via devtools and the onrejectionhandled event) that the rejection was handled.
  • (Note that only one of these two alternatives should ever occur.)

The notify-rejected task, which is given a promise ID and rejection reason, does the following:

  • If the promise ID is no longer in the list of about-to-be-notified rejected promise IDs, do nothing.
  • Otherwise,
    • Notify about a rejection with the given promise ID and rejection reason.
    • Add the given promise ID to the list of outstanding rejected promise IDs.
@licaomeng
Copy link

I am working on an annotation (high-order-component) for Promise full-chain aspect injection. However, there is no top-level hook for Promise error handler. So it seems impossible to achieve it. I'd like to know is there any approach to tackle it or feedback from TC39?

export default foo (...config) => (target, name, descriptor) => {
  const origin = descriptor.value

  descriptor.value = async (...args) => {
    try {
      await origin()
      // do some stuff here
    } catch (e) {
      // do some stuff here
      // if Promise chained with `catch`, code can not be performed through here
    }
  }

  return descriptor
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment