Can we improve async JavaScript error handling?
We already have a construct that automatically returns a promise, which resolves on return and rejects on throw. It's called an async function!
We already have a construct that automatically returns a promise, which resolves on return and rejects on throw. It's called an async function! On Nov 30 2019, at 7:05 am, Lars Eidnes <larseidnes at gmail.com> wrote: > 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? > > _______________________________________________ > es-discuss mailing list > es-discuss at mozilla.org > https://mail.mozilla.org/listinfo/es-discuss > -------------- next part -------------- An HTML attachment was scrubbed... URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20191202/18dbccd1/attachment.html>
Le 29 nov. 2019 à 21:05, Lars Eidnes <larseidnes at gmail.com> a écrit :
- 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?
I think so (although there is no need keep the callback). But since setTimeout() is not part of ECMAScript proper, this is not the proper place to discuss it. See rather:
whatwg/html#617, whatwg/html#617
- Are there other things we could do to minimize the need for resolve/reject?
I don’t think there is anything to do at the core language level, because it has already the necessary tools (async/await). Rather, new APIs ought to be designed in order to be directly usable with await
, i.e., they ought to return a Promise instead of taking a callback or having an event-handler. F.e., the old XMLHttpRequest API shall be replaced with the new Fetch API.
> Le 29 nov. 2019 à 21:05, Lars Eidnes <larseidnes at gmail.com> a écrit : > > > > 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? I think so (although there is no need keep the callback). But since setTimeout() is not part of ECMAScript proper, this is not the proper place to discuss it. See rather: https://github.com/whatwg/html/issues/617 <https://github.com/whatwg/html/issues/617> > > 2) Are there other things we could do to minimize the need for resolve/reject? > I don’t think there is anything to do at the core language level, because it has already the necessary tools (async/await). Rather, new APIs ought to be designed in order to be directly usable with `await`, i.e., they ought to return a Promise instead of taking a callback or having an event-handler. F.e., the old XMLHttpRequest API shall be replaced with the new Fetch API. —Claude -------------- next part -------------- An HTML attachment was scrubbed... URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20191202/a9267ca5/attachment.html>
We already have a construct that automatically returns a promise, which
resolves on return and rejects on throw. It's called an async function!
That's true, but marking a function as async doesn't (on its own) make it a substitute for something like setTimeout.
> We already have a construct that automatically returns a promise, which resolves on return and rejects on throw. It's called an async function! That's true, but marking a function as async doesn't (on its own) make it a substitute for something like setTimeout. On Mon, Dec 2, 2019 at 4:49 AM Frederick Stark <coagmano at gmail.com> wrote: > We already have a construct that automatically returns a promise, which > resolves on return and rejects on throw. It's called an async function! > > > On Nov 30 2019, at 7:05 am, Lars Eidnes <larseidnes at gmail.com> wrote: > > 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? > > _______________________________________________ > es-discuss mailing list > es-discuss at mozilla.org > https://mail.mozilla.org/listinfo/es-discuss > > -------------- next part -------------- An HTML attachment was scrubbed... URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20191202/d0e2e577/attachment.html>
But since setTimeout() is not part of ECMAScript proper, this is not the
proper place to discuss it. See rather:
Thanks Claude for this pointer. The callback-free implementation proposed at the end of that thread seems like it would fix the issues I'm thinking of, without requiring anything new from the language/runtime. Cool.
> But since setTimeout() is not part of ECMAScript proper, this is not the proper place to discuss it. See rather: Thanks Claude for this pointer. The callback-free implementation proposed at the end of that thread seems like it would fix the issues I'm thinking of, without requiring anything new from the language/runtime. Cool. On Mon, Dec 2, 2019 at 10:15 AM Claude Pache <claude.pache at gmail.com> wrote: > > > Le 29 nov. 2019 à 21:05, Lars Eidnes <larseidnes at gmail.com> a écrit : > > > > 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? > > > I think so (although there is no need keep the callback). But since > setTimeout() is not part of ECMAScript proper, this is not the proper place > to discuss it. See rather: > > https://github.com/whatwg/html/issues/617 > > > > 2) Are there other things we could do to minimize the need for > resolve/reject? > > > I don’t think there is anything to do at the core language level, because > it has already the necessary tools (async/await). Rather, new APIs ought to > be designed in order to be directly usable with `await`, i.e., they ought > to return a Promise instead of taking a callback or having an > event-handler. F.e., the old XMLHttpRequest API shall be replaced with the > new Fetch API. > > —Claude > -------------- next part -------------- An HTML attachment was scrubbed... URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20191202/8fdaf891/attachment.html>
Currently when awaiting calls there's some surprising behavior when combining throw/catch and reject/resolve.
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: 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:
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:
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:
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?
Are there other things we could do to minimize the need for resolve/reject?
What do you think?