Can we improve async JavaScript error handling?

Lars Eidnes larseidnes at gmail.com
Fri Nov 29 20:05:15 UTC 2019


Currently when awaiting calls there's some surprising behavior when
combining throw/catch and reject/resolve.

```js
    function resolveAndThrow() {
        return new Promise((resolve, reject) => {
            resolve("huh");
            throw new Error(""); //<-- this throw is executed
        });
    }
    async function callAsync() {
        try {
            await resolveAndThrow();
        } catch (e) {
            console.log("caught error", e); //<-- yet, this is error
logging never happens
        }
    }
```

Here the error logging doesn't happen, because resolve() is called before
the throw, which causes the thrown error to be swallowed. Specifically, the
throw executes, and control flow is broken, but control flow never moves to
the catch {} block. This is a sort of dark corner, where language
mechanisms interact in conflicting ways. Most programmers wont have at
top-of-mind which of resolve and throw will "win" in a situation like the
one above. In fact, I suspect most JavaScript programmers haven't thought
about this issue at all until they encounter it.

I've written a longer post about this error handling issue in JavaScript
some time ago: https://catchjs.com/Docs/AsyncAwait . There I mention how C#
has async/await, but does not have reject/resolve, and thus doesn't really
have this issue. There, exceptions thrown in async operations get
transferred to the whomever is awaiting the Task, and whatever is returned
from the Task is returned to whomever is awaiting it. So throw serves the
purpose of reject(), and return serves the purpose of resolve().

This is nice. It makes the reject/resolve mechanisms redundant, and removes
the paradoxical resolve+throw case shown above. Really, this possible
because the main interface for creating something awaitable (e.g.
Task.Run(() => {}) ) is more similar to setTimeout than to new Promise().
Is it possible to introduce similar mechanisms to JavaScript?

For reference, C# Tasks can be used like this:

```
    var result = await Task.Run(() => { Thread.Sleep(2); return 1; });
    //result now holds 1

    try {
        var result2 = await Task.Run(() => {
            throw new Exception(""); //This error is tracked across
execution boundaries
            return 1;
        });
    } catch (Exception e) {
        Console.WriteLine(e); //This error logging happens, with a full
stack trace from the await to the throw.
    }
```

The lack of resolve/reject has the benefit that there is no chance of
messing up with mixing throw and resolve. A key thing here is that thrown
exceptions are transported across execution contexts to wherever the Task
is awaited.

The reason we have reject/resolve, as far as I can see, is to be able to
transport values and errors across execution context, for example so that
one can do this:

```js
    function resolveInSetTimeout() {
        return new Promise((resolve, reject) => {
            setTimeout(() => resolve(1), 100);
        });
    }
    async function callAsync() {
        var result = await resolveInSetTimeout(); //result now holds 1
    }
```

In JavaScript, the place where an API similar to Task could be useful would
be as a replacement for setTimeout(callback). Some new API that takes a
callback, returns a Promise, and where thrown errors in the callback are
transported back to anyone awaiting the Promise, and where returned values
in the callback resolve the Promise. It seems to me that transporting the
thrown error across execution boundaries requires language/runtime support,
at least if we want to get proper stack traces.

If libraries with async APIs were built on this mechanism, we could use
them and ensure that we can do error tracking for any errors happening
inside setTimeouts. Now we're at the mercy of the library author
remembering to catch the error and passing it to reject().

I've written a lot here, and I suspect this mailing list is better equipped
than I am to figure out what's a good idea in this context. Two questions
to summarize:

1) Is it a good idea to introduce an alternative to setTimeout, where the
distinction is that it returns a Promise, and that return/throw in the
callback take the role of resolve/reject?

2) Are there other things we could do to minimize the need for
resolve/reject?

What do you think?
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20191129/834e5889/attachment.html>


More information about the es-discuss mailing list