async/await improvements
As you point out, exception swallowing is a problem for promises in general. The best way to approach the issue as it applies to async/await is to just solve it for promises. The current direction JS engines are taking is to log unhandled rejections in one way or another. I would keep an eye on those developments.
On Wed Nov 12 2014 at 1:33:52 AM James Long <longster at gmail.com> wrote:
After a brief twitter conversation last night (twitter.com/lbljeffmo/status/532402141001179136) I thought I'd post some thoughts I've been having about async/await.
I feel like I'm about to walk into a pit where people throw lava at each other, but I need to see if at least a few other people agree with me.
I know I'm in the minority here, but I don't like promises behavior of automatically suppressing all errors. But I know there advantages to it. Pros & cons, you know. However, recently there's been talk of adding special builtin
async
andawait
features to ES7. This is cool, except that currently they are built on top of promises, meaning you get the same suppress-error-by-default behavior.Meaning, if I had the following:
async function foo() { throw new Error('bad'); }
You wouldn't see this error unless you remember to somehow "end" the async chain. Am I correct in this?
The thing is, I think we actually have a chance to fix this since async/await will be special builtin syntax. Let's first take a look at C# error handling behavior (which supposedly is what inspired async/await):
async static void AsyncVersion() { // uh oh error happened }
The above code throws the error by default, no special handling needed. Isn't that cool? But what if you want to handle errors asynchronously? Well, C# knows that you don't return anything above, so it knows if can just throw it. If you want to forward the error, you need to return a Task:
async static Task AsyncVersion() { // throw an error }
So there's actually 2 different ways to suggest how to forward/throw errors. This makes async/await insanely cool because you know at some point at the top of chain the error will always throw, since you will always be calling it from a top-level void async function.
Can't we do the same with our async/await? What if we made it only possible to call async functions from other async functions, made
async function
throw by default (not forward), but introducedasync^
which would forward?async^ function foo() { // errors here are captured and forwarded }
async function foo() { // errors here are thrown in the JS processes }
I don't understand how that would work? The exception might not happen in the current turn.
async function foo() { await sleep(1000); throw new Error(); }
Maybe all you were saying is that, if there is an exception before the first await then throw instead of reject?
Not sure of the best way to reply to both emails, guess I'll just inline both here:
On Wed, Nov 12, 2014 at 8:38 AM, Kevin Smith <zenparsing at gmail.com> wrote:
Hi James,
As you point out, exception swallowing is a problem for promises in general. The best way to approach the issue as it applies to async/await is to just solve it for promises. The current direction JS engines are taking is to log unhandled rejections in one way or another. I would keep an eye on those developments.
Yes, I have (I work on the Firefox devtools). The best we can do (as far as I know) is log an error when a promise is GC-ed with an unhandled error, which isn't great in my opinion. It doesn't help at all for production (sure, "you aren't using promises correctly" and all that, but not all programmers are perfect). Also makes it harder for tests, because I don't think we've standardized an onUncaughtPromiseException, and if we have, it's GC-sensitive. Lastly just waiting a few seconds to see your error sucks (who knows when the GC happens, maybe you kept a reference and it never does). You also lose "pause on uncaught exception" since the call context gone.
I don't want fall into a promise war here, but I'm just suggesting an idea for async/await to avoid all of that, though it does change the error handling semantics.
On Wed, Nov 12, 2014 at 9:29 AM, Erik Arvidsson <erik.arvidsson at gmail.com> wrote:
I don't understand how that would work? The exception might not happen in the current turn.
async function foo() { await sleep(1000); throw new Error(); }
Maybe all you were saying is that, if there is an exception before the first await then throw instead of reject?
I don't see why it wouldn't work. An async function returns a deferred
object, which other functions can use await
to wait on. Whenever the
error happens, put it on the deferred object (if using async^
) and
whenever another async
function tries to access it with await
it
will throw the error.
Actually, it is possible we can do this without any extra syntax at
all (no await^
). If we restrict async functions to only be able to
be called by other async functions, the engine should be able to keep
track of the async stack and throw errors appropriately up the chain,
and know when it hits a top-level async function and throw the error
for real automatically.
Can anyone with knowledge of how current JS engines work confirm that?
I think you're missing the point: you need to solve the halting problem if you expect to get prompt notification of asynchronous exceptions. This can't be solved at a language level. The only solution is to expose the pending unhandled exceptions to programmers so that they can use their human judgment to determine when the exception has been pending "long enough" that it's actually an error. (And this isn't a new issue, or specific to promises or await/async.)
GC of an promise tells you that some async exceptions can no longer be handled, but that's only part of the iceberg. Promises can also be kept live indefinitely with unhandled exceptions. That's not necessarily an error -- perhaps a handler will eventually be added. Like I said, you need to solve the halting problem if you want to definitely identified unhandled asynchronous exceptions. The best solution is to expose the pending exceptions to the human, and let them decide.
Please go back and reread the extensive discussions we've had on this topic previously on this mailing list. I'm writing from my phone, but perhaps someone better connected can give you a few URLs.
Perhaps I worded it too strongly: I don't expect to get perfectly prompt notifications of async exceptions. I don't think "pause on uncaught exception" will ever work (to actually pause in the context of where the exception occurred) for exceptions that are meant to be forwarded.
What I'm saying is that by default we can throw the error when it's
accessed by await
. If we restrict async functions to only be called
from other async functions, the engine should know if it's at the
top-level or not, and whether or not to put it on the deferred object
or to literally throw it (assuming the user didn't put try/catch
around it).
This only works if we don't make async functions return promises. This is totally doable and just matter if there's enough consensus on it.
Actually, it is possible we can do this without any extra syntax at all (no
await^
). If we restrict async functions to only be able to be called by other async functions, the engine should be able to keep track of the async stack and throw errors appropriately up the chain, and know when it hits a top-level async function and throw the error for real automatically.
Hmm... One of the nice properties of the current design is that the caller doesn't need to know whether the called function is "async" or not. All it needs to know is whether the function returns a promise. With this setup we'd lose that encapsulation.
Again, this is a generic issue for promises: a good solution there will "fix" everything.
My preferred solution to this problem: modify host-to-user "dispatchers"
so that if user code returns a rejected promise to the host, an error is
reported. So for instance, if user code returns a rejected promise from a
DOM element event handler, the browser would log the error and call
window.onerror
. With async functions, this would work almost
transparently:
element.addEventListener("click", async event => {
throw new Error("boom");
});
The host would pick up the rejection returned from the handler and report the error.
This only works if we don't make async functions return promises. This is totally doable and just matter if there's enough consensus on it.
Well, that's not really going to happen, though. Async functions will be promise-based: it's a core design feature.
Again, this is a generic issue for promises: a good solution there will "fix" everything.
It's inherent in the promise design, there is no way to truly fix promises. You have to mark the end of a promise chain. It's always going to be that way.
My preferred solution to this problem: modify host-to-user "dispatchers" so that if user code returns a rejected promise to the host, an error is reported. So for instance, if user code returns a rejected promise from a DOM element event handler, the browser would log the error and call
window.onerror
. With async functions, this would work almost transparently:element.addEventListener("click", async event => { throw new Error("boom"); });
The host would pick up the rejection returned from the handler and report the error.
If you've ever worked on a complex app that uses promises, you know this doesn't really change anything. Promise chains are everywhere and you have to make sure to end them.
It's inherent in the promise design, there is no way to truly fix promises. You have to mark the end of a promise chain. It's always going to be that way.
So you're starting from the position that promises are inherently flawed and should be abandoned. That's not really constructive discussion at this point.
On Wed, Nov 12, 2014 at 10:46 AM, Kevin Smith <zenparsing at gmail.com> wrote:
It's inherent in the promise design, there is no way to truly fix promises. You have to mark the end of a promise chain. It's always going to be that way.
So you're starting from the position that promises are inherently flawed and should be abandoned. That's not really constructive discussion at this point.
Not necessarily! I know a lot of people like promises and I'm fine with them being in the language! What I'm saying is that if we are going to build special support for async/await we should take a step back and see what we could do if they weren't built on top of promises. That doesn't mean abandoning them completely from the language. It turns out (in my opinion) that we could do better if async/await didn't assume the promise infrastructure.
On 12 Nov 2014, at 16:33, James Long <longster at gmail.com> wrote:
Again, this is a generic issue for promises: a good solution there will "fix" everything.
It's inherent in the promise design, there is no way to truly fix promises. You have to mark the end of a promise chain. It's always going to be that way.
Is that true, though? Couldn’t a finalizer or something similar check (before a promise is garbage collected) whether all errors have been handled?
On Wed, Nov 12, 2014 at 11:08 AM, Axel Rauschmayer <axel at rauschma.de> wrote:
Is that true, though? Couldn’t a finalizer or something similar check (before a promise is garbage collected) whether all errors have been handled?
A finalizer can do this check. This will flag some uncaught exceptions, but not promptly. And as I wrote above, that's only part of the issue -- promises can also be kept alive for an indefinite period of time, but never end up either handling their exceptions or becoming unreachable. This could also be an error.
That is, liveness is one way to tell that an exception will never be handled, but it is only an approximation.
And it's not necessarily an error to not handle an exception --
Promise.race()
is expected to have this behavior as a matter of
course, for example.
We've been through this discussion many times before. Eventually
there may be a Promise#done
. But the consensus was that the first
step was to give the devtools folks time to make good UI for showing
the dynamic "unhandled async exception" state of a program, and see
how well that worked.
--scott
ps. some of the discussed language features threaten to release zalgo. but i'll not open up that can of worms.
On Wed, Nov 12, 2014 at 6:17 PM, C. Scott Ananian <ecmascript at cscott.net>
wrote:
On Wed, Nov 12, 2014 at 11:08 AM, Axel Rauschmayer <axel at rauschma.de> wrote:
Is that true, though? Couldn’t a finalizer or something similar check (before a promise is garbage collected) whether all errors have been handled?
A finalizer can do this check. This will flag some uncaught exceptions, but not promptly. And as I wrote above, that's only part of the issue -- promises can also be kept alive for an indefinite period of time, but never end up either handling their exceptions or becoming unreachable. This could also be an error.
That is, liveness is one way to tell that an exception will never be handled, but it is only an approximation.
And it's not necessarily an error to not handle an exception --
Promise.race()
is expected to have this behavior as a matter of course, for example.We've been through this discussion many times before. Eventually there may be a
Promise#done
. But the consensus was that the first step was to give the devtools folks time to make good UI for showing the dynamic "unhandled async exception" state of a program, and see how well that worked. --scott
Actually that already works, at least in Chrome, if you execute
(function () { return new Promise(function (resolve, reject) { reject(new Error("foo")); }); }());
that shows up as an uncaught exception in the console.
On Wed, Nov 12, 2014 at 12:11 PM, Jussi Kalliokoski <jussi.kalliokoski at gmail.com> wrote:
Actually that already works, at least in Chrome, if you execute
(function () { return new Promise(function (resolve, reject) { reject(new Error("foo")); }); }());
That's a false positive though. It does the same thing with this:
var x = (function () { return new Promise(function (resolve, reject) { reject(new Error("foo")); }); }());
When you could later on attach an error handler to x. We are starting to favor false positives in order to get somewhat immediate error logging. Also none of this helps production code unless we also have something like onUncaughtPromiseException?
On Wed, Nov 12, 2014 at 12:15 PM, James Long <longster at gmail.com> wrote:
On Wed, Nov 12, 2014 at 12:11 PM, Jussi Kalliokoski <jussi.kalliokoski at gmail.com> wrote:
Actually that already works, at least in Chrome, if you execute
(function () { return new Promise(function (resolve, reject) { reject(new Error("foo")); }); }());
That's a false positive though. It does the same thing with this:
var x = (function () { return new Promise(function (resolve, reject) { reject(new Error("foo")); }); }());
When you could later on attach an error handler to x. We are starting to favor false positives in order to get somewhat immediate error logging. Also none of this helps production code unless we also have something like onUncaughtPromiseException?
What happens if you attach an error handler to x. Does the console warning go away? (It should.)
On Wed, Nov 12, 2014 at 12:18 PM, C. Scott Ananian <ecmascript at cscott.net> wrote:
What happens if you attach an error handler to x. Does the console warning go away? (It should.) --scott
It does not. I think that would be weird, but I've heard the idea of "graying out" the logged error which makes sense. Still, your out of luck if this happens in production.
This has derailed a bit but we can have more deterministic errors with async/await with my suggestions... :)
On 11/12/14, 11:08 AM, Axel Rauschmayer wrote:
Is that true, though? Couldn’t a finalizer or something similar check (before a promise is garbage collected) whether all errors have been handled?
It can, but then what? How do you report the rejection value in that finalizer? In your typical GC setup, finalization order is not guaranteed, so the rejection value may already be finalized.
On 11/12/14, 9:49 AM, James Long wrote:
Yes, I have (I work on the Firefox devtools). The best we can do (as far as I know) is log an error when a promise is GC-ed with an unhandled error
Note that this is what Firefox does right now, but we're moving away from that to a "report if the event loop starts spinning while a promise with an unhandled rejection is live" and "withdrawing" the report somehow if it then gets handled.
It doesn't help at all for production (sure, "you aren't using promises correctly" and all that, but not all programmers are perfect).
Note also Domenic's proposal at lists.w3.org/Archives/Public/public-whatwg-archive/2014Sep/0024.html in terms of production. It does involve a bit more work on the part of sites that want to usefully do telemetry for this, of course.
Also makes it harder for tests, because I don't think we've standardized an onUncaughtPromiseException, and if we have, it's GC-sensitive.
Domenic's proposal above is such a thing, and not GC-sensitive.
Now it does have the false positive problem you mention later in this thread, of course.
All of this is not directly related to async/await, except insofar as those are built on top of Promise.
In my year long experience with large code bases using Bluebird promises which do unhandled rejection tracking - I haven't had a single false positive or false negative.
Adding error handlers asynchronously is in practice extremely rare.
Even if async functions are changed to only be callable from an async context or toplevel, they could still be promise-based from an implementation's point of view.
The only thing they couldn't do (under this proposal) is expose the promise being used to userland (to eliminate the chance for userland to hold on to the promise and expect to be able to add an error handler at any time).
On 11/12/14, 9:57 AM, James Long wrote:
Actually, it is possible we can do this without any extra syntax at all (no
await^
). If we restrict async functions to only be able to be called by other async functions, the engine should be able to keep track of the async stack and throw errors appropriately up the chain, and know when it hits a top-level async function and throw the error for real automatically.await
is currently a reserved identifier in ES6 modules to leave the option of doing toplevel awaits in a module body in the future. This is interesting, because it would pave the way for a toplevel-rooted async stack as you describe here...
The only thing they couldn't do (under this proposal) is expose the promise being used to userland (to eliminate the chance for userland to hold on to the promise and expect to be able to add an error handler at any time).
And lose the ability to combine the results of async functions with "all", "race", and any other promise combinator? That's a core strength of the current design.
On Wed, Nov 12, 2014 at 1:10 PM, Kevin Smith <zenparsing at gmail.com> wrote:
The only thing they couldn't do (under this proposal) is expose the promise being used to userland (to eliminate the chance for userland to hold on to the promise and expect to be able to add an error handler at any time).
And lose the ability to combine the results of async functions with "all", "race", and any other promise combinator? That's a core strength of the current design.
Yes, having async functions bottom out into promises was not an incidental "oh, why not, we've got them lying around" decision. It was done because Promises are the way we handle and compose async operations, and they're quite useful in a number of ways. We can't lose that without losing a lot of functionality, especially as Promises get more and more built out and are used more in the language and the DOM.
On 11/12/14, 4:10 PM, Kevin Smith wrote:
The only thing they couldn't do (under this proposal) is expose the promise being used to userland (to eliminate the chance for userland to hold on to the promise and expect to be able to add an error handler at any time).
And lose the ability to combine the results of async functions with "all", "race", and any other promise combinator? That's a core strength of the current design.
A very good point.
Crazy, half-baked idea: Move the "forwards" vs "throws/logs" distinction to the callsite (in sync contexts only?) rather than the definition context as was described at the beginning of this thread.
The thought-process running along the lines of making the default behavior to log/report, but with an escape hatch when logging is not what you want. It's easier to notice an undesired log (and suppress it if necessary) than it is to notice that a log is missing in exceptional circumstances.
async function asyncLibrary(data) {
// ...
}
function doStuff() {
// Becomes sugar for something like
// asyncLibrary(badData).catch(err => {
// console.error(err);
// throw err;
// });
//
// This makes logging-to-console the default
var toplevelResult = asyncLibrary(badData);
// No additional .catch() -- just pushes new syntax to discourage
// swallowed-by-default. When you want this "storing" behavior, you
simply
// opt-in to it via syntax (or a stdlib?) rather than opting out
var storedResult = ^asyncLibrary(badData);
// For combinators, you get something like:
Promise.all([
^asyncLibrary(data1),
^asyncLibrary(data2)
]);
}
async function doOtherAsyncStuff() {
// This stays status quo
try {
asyncLibrary(badData);
} catch (e) {
// caught!
}
}
(Frankly the ^
syntax I used here seems a bit arcane to me -- but
maybe someone can see through it and can think of a more expressive syntax?)
On Wed, Nov 12, 2014 at 2:15 PM, Jeff Morrison <lbljeffmo at gmail.com> wrote:
On 11/12/14, 4:10 PM, Kevin Smith wrote:
The only thing they couldn't do (under this proposal) is expose the promise being used to userland (to eliminate the chance for userland to hold on to the promise and expect to be able to add an error handler at any time).
And lose the ability to combine the results of async functions with "all", "race", and any other promise combinator? That's a core strength of the current design.
A very good point.
Crazy, half-baked idea: Move the "forwards" vs "throws/logs" distinction to the callsite (in sync contexts only?) rather than the definition context as was described at the beginning of this thread.
This is already the case. If you want the promise (which "forwards" errors), just call the async function normally. If you want errors to throw through you like a sync function would, use the "await" expression (which requires you to be async as well).
Logging vs forwarding will likely be distinguished via the .done() method, when that happens. That means the default is to forward errors, and you have to explicitly opt into logging them (or hope that the GC catches them and they're auto-logged).
async function doOtherAsyncStuff() { // This stays status quo try { asyncLibrary(badData); } catch (e) { // caught! } }
You need an "await" here, like await asyncLibrary(badData);
, to get
the error to turn back into a throw.
On 11/12/14, 5:23 PM, Tab Atkins Jr. wrote:
On Wed, Nov 12, 2014 at 2:15 PM, Jeff Morrison <lbljeffmo at gmail.com> wrote:
On 11/12/14, 4:10 PM, Kevin Smith wrote:
The only thing they couldn't do (under this proposal) is expose the promise being used to userland (to eliminate the chance for userland to hold on to the promise and expect to be able to add an error handler at any time). And lose the ability to combine the results of async functions with "all", "race", and any other promise combinator? That's a core strength of the current design.
A very good point.
Crazy, half-baked idea: Move the "forwards" vs "throws/logs" distinction to the callsite (in sync contexts only?) rather than the definition context as was described at the beginning of this thread. This is already the case. If you want the promise (which "forwards" errors), just call the async function normally. If you want errors to throw through you like a sync function would, use the "await" expression (which requires you to be async as well).
Indeed you could convert the entire signature of your calling function (and incur all the effects on the downstream call sites in doing so), but I was aiming for a less intrusive escape hatch. Additionally, if you're ever operating at a toplevel somewhere, you don't really have this option.
Logging vs forwarding will likely be distinguished via the .done() method, when that happens. That means the default is to forward errors, and you have to explicitly opt into logging them (or hope that the GC catches them and they're auto-logged).
The problem with the GC-based logging is only that it doesn't work all the time...and for non-obvious reasons. It's really more of a heuristic. The extreme case where it doesn't work has already been mentioned (if you want to attach an error handler later); But a less extreme case is one where you've done something with the promise object that [unintuitively] causes it not to be GC'd (read: a memory leak). It's not so difficult to do this on accident either -- and having such a non-local effect occur would be less than ideal.
async function doOtherAsyncStuff() { // This stays status quo try { asyncLibrary(badData); } catch (e) { // caught! } }
You need an "await" here, like
await asyncLibrary(badData);
, to get the error to turn back into a throw.
Oops, nice catch. That was a typo.
On 11/12/14, 5:49 PM, Jeff Morrison wrote:
The problem with the GC-based logging is only that it doesn't work all the time...and for non-obvious reasons.
That's not the only problem. It's also very problematic to implement without imposing nasty constraints on your GC.
On Wed, Nov 12, 2014 at 2:49 PM, Jeff Morrison <lbljeffmo at gmail.com> wrote:
On 11/12/14, 5:23 PM, Tab Atkins Jr. wrote:
On Wed, Nov 12, 2014 at 2:15 PM, Jeff Morrison <lbljeffmo at gmail.com> wrote:
Crazy, half-baked idea: Move the "forwards" vs "throws/logs" distinction to the callsite (in sync contexts only?) rather than the definition context as was described at the beginning of this thread.
This is already the case. If you want the promise (which "forwards" errors), just call the async function normally. If you want errors to throw through you like a sync function would, use the "await" expression (which requires you to be async as well).
Indeed you could convert the entire signature of your calling function (and incur all the effects on the downstream call sites in doing so), but I was aiming for a less intrusive escape hatch. Additionally, if you're ever operating at a toplevel somewhere, you don't really have this option.
It's impossible to rethrow errors without use of "await" - the error may happen in a different turn entirely than the function call. You must convert your calling function into an async one, so that it can do the "freeze and wait for the promise to resolve" thing, which means that it needs to return a promise for itself as well.
On Wed, Nov 12, 2014 at 5:56 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
It's impossible to rethrow errors without use of "await" - the error may happen in a different turn entirely than the function call. You must convert your calling function into an async one, so that it can do the "freeze and wait for the promise to resolve" thing, which means that it needs to return a promise for itself as well.
~TJ
The crucial different from what you're describing (not directly in the paragraph above) is that you say propagate by default, and I say throw by default (and a real throw, like uncaught exception in the debugger type throw). The latter imposes way less constraints on the GC and less ambiguity of error handling, at the cost of slightly more verbose async handling. I was hoping async/await may be a chance to look at this, but it sounds like we're far down the road of promises for it to be an option.
On Wed, Nov 12, 2014 at 3:06 PM, James Long <longster at gmail.com> wrote:
On Wed, Nov 12, 2014 at 5:56 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
It's impossible to rethrow errors without use of "await" - the error may happen in a different turn entirely than the function call. You must convert your calling function into an async one, so that it can do the "freeze and wait for the promise to resolve" thing, which means that it needs to return a promise for itself as well.
The crucial different from what you're describing (not directly in the paragraph above) is that you say propagate by default, and I say throw by default (and a real throw, like uncaught exception in the debugger type throw). The latter imposes way less constraints on the GC and less ambiguity of error handling, at the cost of slightly more verbose async handling. I was hoping async/await may be a chance to look at this, but it sounds like we're far down the road of promises for it to be an option.
No, you're misunderstanding me, or the way that async stuff works.
Calling an async function returns immediately. The called function doesn't actually run until a later turn. If it throws, there's no way, even theoretically, to throw that error at the call-site, because the program counter is already well past that point.
If you want the call-site to throw, then you need the callsite itself to be in an async function, and you need to use the "await" expression, which pauses execution of the caller until the callee's returned promise settles. At that point you can throw the rejection value, or return the fulfillment value.
This isn't something you can make an arbitrary decision on; it's not a syntax matter. It fundamentally changes the way your code works, and you cant' get around it.
The only thing we can actually talk about changing one way or the other is whether unhandled rejections immediately go to the console, or if they only do so upon GC/when you call .done(). That really is something that we can vary just by messing around with syntax.
On Wed, Nov 12, 2014 at 6:18 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
No, you're misunderstanding me, or the way that async stuff works.
Calling an async function returns immediately. The called function doesn't actually run until a later turn. If it throws, there's no way, even theoretically, to throw that error at the call-site, because the program counter is already well past that point.
If you want the call-site to throw, then you need the callsite itself to be in an async function, and you need to use the "await" expression, which pauses execution of the caller until the callee's returned promise settles. At that point you can throw the rejection value, or return the fulfillment value.
Trust me in that I've done a lot of async coding and I understand well how it works. The thing I may not understand fully is the current async/await spec.
The difference is what happens with await
, does it throw or does it
automatically put the error on the promise returned from the async
function. I'm essentially saying it should throw by default and you
need to manually forward it. That's all. It doesn't really break
anything.
The only thing you can ever get with async is a single stack frame (unless you are logging long stack traces), but with that single stack frame you could still do "pause on uncaught exception" and see the local state for at least that.
When using async/await, how do you tell an async function that it's the top-level one and it should throw all errors (the equivalent to .done())? I don't see that anywhere.
On Wed, Nov 12, 2014 at 3:29 PM, James Long <longster at gmail.com> wrote:
On Wed, Nov 12, 2014 at 6:18 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
No, you're misunderstanding me, or the way that async stuff works.
Calling an async function returns immediately. The called function doesn't actually run until a later turn. If it throws, there's no way, even theoretically, to throw that error at the call-site, because the program counter is already well past that point.
If you want the call-site to throw, then you need the callsite itself to be in an async function, and you need to use the "await" expression, which pauses execution of the caller until the callee's returned promise settles. At that point you can throw the rejection value, or return the fulfillment value.
Trust me in that I've done a lot of async coding and I understand well how it works. The thing I may not understand fully is the current async/await spec.
The difference is what happens with
await
, does it throw or does it automatically put the error on the promise returned from the async function. I'm essentially saying it should throw by default and you need to manually forward it. That's all. It doesn't really break anything.
Something's still going wrong in your understanding here. "await" receives a promise. It doesn't return one. It either returns the fulfillment value or throws the rejection value. The question you're asking doesn't make sense.
On Wed, Nov 12, 2014 at 6:34 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
On Wed, Nov 12, 2014 at 3:29 PM, James Long <longster at gmail.com> wrote:
On Wed, Nov 12, 2014 at 6:18 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
No, you're misunderstanding me, or the way that async stuff works.
Calling an async function returns immediately. The called function doesn't actually run until a later turn. If it throws, there's no way, even theoretically, to throw that error at the call-site, because the program counter is already well past that point.
If you want the call-site to throw, then you need the callsite itself to be in an async function, and you need to use the "await" expression, which pauses execution of the caller until the callee's returned promise settles. At that point you can throw the rejection value, or return the fulfillment value.
Trust me in that I've done a lot of async coding and I understand well how it works. The thing I may not understand fully is the current async/await spec.
The difference is what happens with
await
, does it throw or does it automatically put the error on the promise returned from the async function. I'm essentially saying it should throw by default and you need to manually forward it. That's all. It doesn't really break anything.Something's still going wrong in your understanding here. "await" receives a promise. It doesn't return one. It either returns the fulfillment value or throws the rejection value. The question you're asking doesn't make sense.
~TJ
await
is always inside an async
function so there's always a
promise created for that function which is waiting for it to be done
executing. That's the one I'm talking about.
On Wed, Nov 12, 2014 at 3:36 PM, James Long <longster at gmail.com> wrote:
await
is always inside anasync
function so there's always a promise created for that function which is waiting for it to be done executing. That's the one I'm talking about.
Okay. That doesn't change my response. The outer async function also
returns a promise, and doesn't run syncly with its caller, so it's
again physically impossible for it to throw an error at its callsite
(again, unless its caller has opted into asynchrony and used
await
).
On Wed, Nov 12, 2014 at 6:46 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
On Wed, Nov 12, 2014 at 3:36 PM, James Long <longster at gmail.com> wrote:
await
is always inside anasync
function so there's always a promise created for that function which is waiting for it to be done executing. That's the one I'm talking about.Okay. That doesn't change my response. The outer async function also returns a promise, and doesn't run syncly with its caller, so it's again physically impossible for it to throw an error at its callsite (again, unless its caller has opted into asynchrony and used
await
).~TJ
Yeah, I'm only talking about call sites that used await
. Although,
mainly the question is when an error happens inside an async
function, whether it throws at that point or passes it onto the
returning promise. You're definitely going to view that differently
whether you embrace promises or not.
But we may have gotten past the point of returns in this discussion. Wish we could talk it out in person. :)
<3
On Wed, Nov 12, 2014 at 3:53 PM, James Long <longster at gmail.com> wrote:
On Wed, Nov 12, 2014 at 6:46 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
On Wed, Nov 12, 2014 at 3:36 PM, James Long <longster at gmail.com> wrote:
await
is always inside anasync
function so there's always a promise created for that function which is waiting for it to be done executing. That's the one I'm talking about.Okay. That doesn't change my response. The outer async function also returns a promise, and doesn't run syncly with its caller, so it's again physically impossible for it to throw an error at its callsite (again, unless its caller has opted into asynchrony and used
await
).Yeah, I'm only talking about call sites that used
await
. Although, mainly the question is when an error happens inside anasync
function, whether it throws at that point or passes it onto the returning promise. You're definitely going to view that differently whether you embrace promises or not.
If both the outer and inner callsites used "await", and the innermost callee returned a rejected promise, then the inner "await" will throw, which causes its containing function to reject its promise, which causes the outer "await" to throw (which then causes the outer function to reject its promise).
I'm still confused about what you are confused about here. You get
throws if you use await
. You get rejected promises if you don't.
There's no two ways about it.
On Wed, Nov 12, 2014 at 6:58 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
On Wed, Nov 12, 2014 at 3:53 PM, James Long <longster at gmail.com> wrote:
On Wed, Nov 12, 2014 at 6:46 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
On Wed, Nov 12, 2014 at 3:36 PM, James Long <longster at gmail.com> wrote:
await
is always inside anasync
function so there's always a promise created for that function which is waiting for it to be done executing. That's the one I'm talking about.Okay. That doesn't change my response. The outer async function also returns a promise, and doesn't run syncly with its caller, so it's again physically impossible for it to throw an error at its callsite (again, unless its caller has opted into asynchrony and used
await
).Yeah, I'm only talking about call sites that used
await
. Although, mainly the question is when an error happens inside anasync
function, whether it throws at that point or passes it onto the returning promise. You're definitely going to view that differently whether you embrace promises or not.If both the outer and inner callsites used "await", and the innermost callee returned a rejected promise, then the inner "await" will throw, which causes its containing function to reject its promise, which causes the outer "await" to throw (which then causes the outer function to reject its promise).
Right, I'm saying that the "throw" at the inner await
would be a an
actual uncaught exception unless the async function has been
explicitly marked as something that should forward errors (i.e. a
function that returns a promise). I prefer marking things to propagate
rather then marking the end of the chain (.done()). That's the big
difference that opposes the promise-style behavior, and is why it's
probably hard for you to understand what I mean.
(It's the difference between C# async static void AsyncVersion() {}
and async static Task AsyncVersion() {}
).
Maybe this would be resolved if you could answer this: how do you mark an async function to be a top-level one? I don't see anywhere that says "I don't return a promise, I want errors inside of me to literally throw. I am an all-powerful top-level consumer of async stuff"). Seems like that would need extra syntax?
On Wed, Nov 12, 2014 at 4:07 PM, James Long <longster at gmail.com> wrote:
Maybe this would be resolved if you could answer this: how do you mark an async function to be a top-level one? I don't see anywhere that says "I don't return a promise, I want errors inside of me to literally throw. I am an all-powerful top-level consumer of async stuff"). Seems like that would need extra syntax?
There is currently no way to do so. If there was, they still wouldn't "literally throw", because again, that's impossible - by the time the promise rejects, it may be another turn entirely, and the program counter is long past the callsite. The best it can do is be an automatically-unhandled error, caught by window.onerror.
On Wed, Nov 12, 2014 at 7:14 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
On Wed, Nov 12, 2014 at 4:07 PM, James Long <longster at gmail.com> wrote:
Maybe this would be resolved if you could answer this: how do you mark an async function to be a top-level one? I don't see anywhere that says "I don't return a promise, I want errors inside of me to literally throw. I am an all-powerful top-level consumer of async stuff"). Seems like that would need extra syntax?
There is currently no way to do so. If there was, they still wouldn't "literally throw", because again, that's impossible - by the time the promise rejects, it may be another turn entirely, and the program counter is long past the callsite. The best it can do is be an automatically-unhandled error, caught by window.onerror.
You do realize that generators have a throw
method, and way long
after the generator is yielded you can throw and error from the yield
point and current devtools will correctly pause at that point if you
enable "pause on uncaught exceptions"? A top-level async function
would throw in exactly this same way when an a promise that await
it
waiting for fails.
This really seems like a huge oversight that there isn't a way to mark
an async function as top-level. When async/await gets here people are
going to want to use that everywhere, as they should, and forcing
them to only interact with them as middle-men just so that you can
call .done()
on an old-style promise chain is weird.
On Wed, Nov 12, 2014 at 4:18 PM, James Long <longster at gmail.com> wrote:
On Wed, Nov 12, 2014 at 7:14 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
On Wed, Nov 12, 2014 at 4:07 PM, James Long <longster at gmail.com> wrote:
Maybe this would be resolved if you could answer this: how do you mark an async function to be a top-level one? I don't see anywhere that says "I don't return a promise, I want errors inside of me to literally throw. I am an all-powerful top-level consumer of async stuff"). Seems like that would need extra syntax?
There is currently no way to do so. If there was, they still wouldn't "literally throw", because again, that's impossible - by the time the promise rejects, it may be another turn entirely, and the program counter is long past the callsite. The best it can do is be an automatically-unhandled error, caught by window.onerror.
You do realize that generators have a
throw
method, and way long after the generator is yielded you can throw and error from the yield point and current devtools will correctly pause at that point if you enable "pause on uncaught exceptions"? A top-level async function would throw in exactly this same way when an a promise thatawait
it waiting for fails.
Yes, that's exactly what I mean by "automatically-unhandled error". It's impossible to catch an asynchronous error from the synchronous code that called it, but you can appeal to a global environment to catch it.
This really seems like a huge oversight that there isn't a way to mark an async function as top-level. When async/await gets here people are going to want to use that everywhere, as they should, and forcing them to only interact with them as middle-men just so that you can call
.done()
on an old-style promise chain is weird.
Marking a function as "top-level" is just syntax sugar for calling .done() on the returned promise. (Assuming that .done() can forward rejections to window.onerror.) It might be useful, I dunno, but it doesn't offer anything fundamentally new.
On Wed, Nov 12, 2014 at 7:24 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
Marking a function as "top-level" is just syntax sugar for calling .done() on the returned promise. (Assuming that .done() can forward rejections to window.onerror.) It might be useful, I dunno, but it doesn't offer anything fundamentally new.
It would be syntax sugar for:
(async function foo() { var x = await bar(); var y = await baz(); })().done();
Imaging writing lots of async code, where there are lots of top-level async functions... that seems like a wild thing to impose on users.
On Wed, Nov 12, 2014 at 7:28 PM, James Long <longster at gmail.com> wrote:
On Wed, Nov 12, 2014 at 7:24 PM, Tab Atkins Jr. <jackalmage at gmail.com> wrote:
Marking a function as "top-level" is just syntax sugar for calling .done() on the returned promise. (Assuming that .done() can forward rejections to window.onerror.) It might be useful, I dunno, but it doesn't offer anything fundamentally new.
It would be syntax sugar for:
(async function foo() { var x = await bar(); var y = await baz(); })().done();
Imaging writing lots of async code, where there are lots of top-level async functions... that seems like a wild thing to impose on users.
But even better, if we have that syntax sugar the JS engine can
automatically throw from the point of the await
that failed. Think
of it exactly like Task.spawn
or Promise.spawn
in current promise
libs.
I need to run for the night. I think we've each made our points though.
Note that in my "crazy idea" I didn't say rethrow -- I carefully called it out as more of a "log" than a throw.
Sent from my iPhone
On Thu, Nov 13, 2014 at 1:57 AM, Jeff Morrison <lbljeffmo at gmail.com> wrote:
Note that in my "crazy idea" I didn't say rethrow -- I carefully called it out as more of a "log" than a throw.
I thought the plan was to have something equivalent to window.onerror for promises.
On 11/13/14, 4:25 AM, Anne van Kesteren wrote:
On Thu, Nov 13, 2014 at 1:57 AM, Jeff Morrison <lbljeffmo at gmail.com> wrote:
Note that in my "crazy idea" I didn't say rethrow -- I carefully called it out as more of a "log" than a throw. I thought the plan was to have something equivalent to window.onerror for promises.
I think that's what this discussion is about. Such a "log" could be dispatched to window.onerror as well as console (etc).
Coming back to this after sleeping on it, though: The biggest problem I can think of with my particular idea is that it'd require that we somehow encode the concept of window.onerror and/or the console (etc) into the spec -- which currently has no notion of these things.
On Thu, Nov 13, 2014 at 7:13 PM, Jeff Morrison <lbljeffmo at gmail.com> wrote:
I think that's what this discussion is about. Such a "log" could be dispatched to window.onerror as well as console (etc).
Coming back to this after sleeping on it, though: The biggest problem I can think of with my particular idea is that it'd require that we somehow encode the concept of window.onerror and/or the console (etc) into the spec -- which currently has no notion of these things.
lists.w3.org/Archives/Public/public-whatwg-archive/2014Sep/thread.html#msg24
It seems in scope for the specification that defines window.onerror...
Someone is working on standardizing parts of the console API as well. I think it's required these days since it leaked on the web...
On Thu, Nov 13, 2014 at 7:42 PM, Anne van Kesteren <annevk at annevk.nl> wrote:
lists.w3.org/Archives/Public/public-whatwg-archive/2014Sep/thread.html#msg24
It seems in scope for the specification that defines window.onerror...
Someone is working on standardizing parts of the console API as well. I think it's required these days since it leaked on the web.
One challenge I've had with window.onerror is catching the errors in
another window (or iframe), since it's not possible to attach an onerror
handler before the window content loads (because it is overwritten when the
window content loads). The result is that the parent window cannot catch
errors in the child window without the child window opting into it, by
setting window.onerror = window.parent.onerror
. If window.onerror and a
possble window.onPromiseCatch is to be standardized then it should be
possible for one window to handle the errors in child windows (taking same
origin and CSP into consideration ofcourse). This should probably be
generalized to realms, so that one realm can catch all the errors in
another realm.
Marius Gundersen
After a brief twitter conversation last night (twitter.com/lbljeffmo/status/532402141001179136) I thought I'd post some thoughts I've been having about async/await.
I feel like I'm about to walk into a pit where people throw lava at each other, but I need to see if at least a few other people agree with me.
I know I'm in the minority here, but I don't like promises behavior of automatically suppressing all errors. But I know there advantages to it. Pros & cons, you know. However, recently there's been talk of adding special builtin
async
andawait
features to ES7. This is cool, except that currently they are built on top of promises, meaning you get the same suppress-error-by-default behavior.Meaning, if I had the following:
async function foo() { throw new Error('bad'); }
You wouldn't see this error unless you remember to somehow "end" the async chain. Am I correct in this?
The thing is, I think we actually have a chance to fix this since async/await will be special builtin syntax. Let's first take a look at C# error handling behavior (which supposedly is what inspired async/await):
async static void AsyncVersion() { // uh oh error happened }
The above code throws the error by default, no special handling needed. Isn't that cool? But what if you want to handle errors asynchronously? Well, C# knows that you don't return anything above, so it knows if can just throw it. If you want to forward the error, you need to return a Task:
async static Task AsyncVersion() { // throw an error }
So there's actually 2 different ways to suggest how to forward/throw errors. This makes async/await insanely cool because you know at some point at the top of chain the error will always throw, since you will always be calling it from a top-level void async function.
Can't we do the same with our async/await? What if we made it only possible to call async functions from other async functions, made
async function
throw by default (not forward), but introducedasync^
which would forward?async^ function foo() { // errors here are captured and forwarded }
async function foo() { // errors here are thrown in the JS processes }
This makes it the default behavior to be "forgot to forward error" instead of "forgot to log the error", which is better imho. I'm sure there are problems with this, and the community might hate it. If so please let's just move on and not make this a holy war. I can accept that the JS community has already embraced promises-style error handling. But I thought I'd throw this quick idea out there.
With <3,