tc39/proposal-async-context

Storing the context in which an exception was thrown in sync code

andreubotella opened this issue · 0 comments

While investigating the web integration of AsyncContext, I noticed that the error event on the window object would get fired with an originating context that might be unexpected. As a reminder, this event gets fired when an uncaught exception gets thrown in synchronous JS code – equivalent to process.on("uncaughtException") in Node.js.

function cb() {
  asyncVar.run("foo", () => {
    throw new Error();
  });
}

asyncVar.run("bar", () => {
  setTimeout(cb, 0);  // setTimeout fires an `error` event if `cb()` throws
});

// For the purposes of this discussion, let's assume that `error` event
// listeners always use the originating context.
window.addEventListener("error", () => {
  console.log(asyncVar.get());  // bar
});

With the current proposed spec text, the only thing that could be logged here is "bar" (or undefined). This is because the .run() methods always restore the previous context when the callback finishes running, whether that is a return or a throw. And therefore, the "foo" context gets lost before the code inside inside setTimeout that fires the error event is reached. (Note that unhandledrejection can keep the "foo" context.)

If we agree that the expected result here is "foo", then we could change the spec so that if a callback passed to .run() throws (synchronously), the context inside the run gets stored, and the last thrown context could be exposed to web specs. However, could it be worth exposing this to userland? (This might benefit at the very least Node.js and Deno, since their error event implementation happens in JS code, although that code would of course have access to features from the V8 embedding API.) If so, how would that API behave inside async functions?