await on synchronous functions
I know the spec for this isn't finalized, but what is the current direction for the behaviour when await is used on a function that is not marked async and doesn't return a Promise? Should it run immediately or wait for the next turn of the event loop?
More generally, the question is: what should await do for non-promises?
await 1;
Should it force a job to be queued?
If I might, if there's one thing that has never particularly shone in JS, that is consistency.
I see only two possibilities here: 1) it throws with non Promises 2) it
"Promisify" anything that's not a Promise as if it was a
Promise.resolve(1)
... but since there's too much magic in the second
point, I'd rather stick with the first one.
Just my quick thoughts
Best
2015-07-17 19:41 GMT+02:00 Andrea Giammarchi <andrea.giammarchi at gmail.com>:
If I might, if there's one thing that has never particularly shone in JS, that is consistency.
I see only two possibilities here: 1) it throws with non Promises 2) it "Promisify" anything that's not a Promise as if it was a
Promise.resolve(1)
... but since there's too much magic in the second point, I'd rather stick with the first one.
I would be highly in favor of (2). Think about a large program where you refactor a single async function to no longer be async. Then I see no reason why I should be forced to refactor all of its callers to remove the await keyword. Going from sync to async requires refactoring because you're introducing new potential interleaving hazards, but any code that is already prepared to work with async functions (or promises in general) should work equally fine on immediately resolved promises.
, Tom
Think about a large program where you refactor a single async function to
no longer be async
did that ever happened in the history of logic? I am actually curious to understand a single valid case where that would be a solution to any problem.
Apologies if I can't see your point but we've been talking about "Promise must Promise" so much this answer was absolutely unexpected.
Thanks for any sort of clarification
I would tend to agree with Tom (i.e. Promisify non-promises), even though we should notice that awaiting a synchronous function adds a non-trivial overhead, because although the promise is immediately resolved, the await will "return" only after being processed in the event loop (if I remember the Promise spec correctly).
So if option #1 has the advantage of "alerting" us to this performance effect, why do I still prefer option #2? Because of consistency - this is similar to the callback the ".then" of a promise accepts: if the callback returns a promise, it is chained, and if it is a regular value, then it is promisified and chained.
The same two options exist there, and the spec decided on #2. And rightfully so. I like the fact that the code "just works" and in languages like JS, I would prefer it "just working" than being "statically correct". Dynamic languages lots of time tend to prefer the "just works, albeit slowly" approach to the "theoretically and statically correct" approach.
If we stick with the rule that await is only regarded as a keyword if it appears in the body of an async function, then await x without async is simply a syntax error, and we can avoid having to answer this question!
That said, perhaps a more natural way of handling wayward await expressions is to treat them as referring to the closest enclosing async function on the call stack (not necessarily the immediate enclosing function), throwing an exception if there is no async function on the stack. Then any await expression would delay the resolution of the Promise returned by whatever async function is currently executing. The same-function-body syntax restriction is a special case of that more general model (and notably easier to implement by transpiling to generators!).
Generalizing async/await in this way turns out to be equivalent to introducing coroutines into the language, and while I would love to see that happen one day (it would greatly simplify writing parallel forEach loops, for example), it would require substantial changes to the execution model of the language.
Here are some slides from a talk I gave earlier this year about the benefits and pitfalls of coroutines, in case you're interested: benjamn.github.io/goto2015
I think this is a valid use case for await working with synchronous functions. Suppose I have an array of functions that I want to call in series. Some are marked async, some return a promise, and some do neither. I'd like to be able to do something like this:
for (let fn of myFunctions) { let result = await fn(); // Do something with result. }
I think we're confusing two different cases here:
- usage of
await
in the body of a function that is not itself marked asasync
- usage of
await f()
wheref
is not marked asasync
.
1 is easy to mark as an early error (and should be imo). 2, not so much (and is what Mark was asking?)
Yes, I am asking about case #2. Thanks for clarifying Chris. For what it's worth, Traceur allows this now.
Having addressed (1) earlier, perhaps by mistake, my thought on (2) is that you probably want await f() to work the same way if f() just happens to return a Promise, as when f is actually async, but there is no way to know, in general, if some arbitrary function will or will not return a Promise, so await should treat all arguments in the same way: by calling Promise.resolve.
It's an open question whether a sufficiently smart runtime could optimize cases when the argument is not a Promise, or when the Promise is already resolved, but it would have to do so under the constraint of not revealing that optimization to the user.
Going back to my original question, suppose I write this:
for (let fn of myFunctions) { let result = await fn(); // Do something with result. }
If all the functions happen to be synchronous and take a while to complete, am I blocking the event loop? I think what I'd like to happen is for each call to happen in the next pass through the event loop.
Yep, all current implementations that I know of (Traceur, Babel/Regenerator, various other generator-based approaches) do what you want, because Promise.resolve(fn()).then(result => ...) always runs the
callback a future turn of the event loop.
Please note I might not be using the term "turn" precisely here (microtask?); my point is just that your example would not be blocking the event loop, and that's a good thing!
Wait for the next turn/job or the event loop.
On Fri, Jul 17, 2015 at 10:33 AM, Kevin Smith <zenparsing at gmail.com> wrote:
I know the spec for this isn't finalized, but what is the current
direction for the behaviour when await is used on a function that is not marked async and doesn't return a Promise? Should it run immediately or wait for the next turn of the event loop?
More generally, the question is: what should await do for non-promises?
await 1;
Should it force a job to be queued?
Yes, absolutely. It must be equivalent to Promise.resolve(1).then(..success-continuation-of-await.., ..error-continuation-of-await..) though of course for the original bindings of Promise.resolve and .then rather than the current bindings.
On Fri, Jul 17, 2015 at 10:41 AM, Andrea Giammarchi < andrea.giammarchi at gmail.com> wrote:
If I might, if there's one thing that has never particularly shone in JS, that is consistency.
I see only two possibilities here: 1) it throws with non Promises 2) it "Promisify" anything that's not a Promise as if it was a
Promise.resolve(1)
... but since there's too much magic in the second point, I'd rather stick with the first one.
Definitely #2. Had #1 been proposed, async/await never would have achieved consensus.
what do you mean by "Promise must Promise"? I've never seen this phrase before.
On Fri, Jul 17, 2015 at 12:30 PM, Ben Newman <benjamin at cs.stanford.edu>
wrote:
If we stick with the rule that await is only regarded as a keyword if it appears in the body of an async function, then await x without async is simply a syntax error, and we can avoid having to answer this question!
Two more possible contexts:
-
We've talked about allowing await at the top level of modules, I think so that the await continuation could proceed after the synchronous part of the load. I am unclear on the details and cannot reconstruct a sensible story from memory.
-
Jafar and I will be making a proposal for allowing await within function* generator functions. No need to debate this until we make the proposal, but it does not alter what await would do inside an async function.
That said, perhaps a more natural way of handling wayward await expressions is to treat them as referring to the closest enclosing async function on the call stack (not necessarily the immediate enclosing function), throwing an exception if there is no async function on the stack.
This would have the same hazards as deep generators: Intermediate functions on the call stack could be postponed till later turns/jobs without their knowledge, causing violations of their assumptions. The event-loop model makes strong guarantees about possible interleaving. We defined generators and async functions as shallow to preserve those guarantees.
Then any await expression would delay the resolution of the Promise returned by whatever async function is currently executing. The same-function-body syntax restriction is a special case of that more general model (and notably easier to implement by transpiling to generators!).
Generalizing async/await in this way turns out to be equivalent to introducing coroutines into the language,
Exactly! Same think for deep generators. That's why we must not do either.
and while I would love to see that happen one day (it would greatly simplify writing parallel forEach loops, for example), it would require substantial changes to the execution model of the language.
Here are some slides from a talk I gave earlier this year about the benefits and pitfalls of coroutines, in case you're interested: benjamn.github.io/goto2015-talk
I'll take a look. But I've already looked at fibers themselves and rejected them on this basis.
I would expect any implementation to do #2 since that's what the draft spec says.
On Fri, Jul 17, 2015 at 12:44 PM, Ben Newman <benjamin at cs.stanford.edu>
wrote:
Having addressed (1) earlier, perhaps by mistake, my thought on (2) is that you probably want await f() to work the same way if f() just happens to return a Promise, as when f is actually async, but there is no way to know, in general, if some arbitrary function will or will not return a Promise, so await should treat all arguments in the same way: by calling Promise.resolve.
It's an open question whether a sufficiently smart runtime could optimize cases when the argument is not a Promise, or when the Promise is already resolved, but it would have to do so under the constraint of not revealing that optimization to the user.
Exactly. And thanks for pointing it out. Specs traffic only in observable differences. In those rare circumstances when an implementation knows that there's no observable difference between .then and .asap, it can optimize. But only then.
Changing a function from returning a Promise<T> to returning a T is just
the same as changing it to return any other type. Your type annotations and refactoring tools should assist you in changing call-sites.
-1 for awaiting non-Promises, from me.
On Fri, Jul 17, 2015 at 12:50 PM, Mark Volkmann <r.mark.volkmann at gmail.com>
wrote:
Going back to my original question, suppose I write this:
for (let fn of myFunctions) { let result = await fn(); // Do something with result. }
If all the functions happen to be synchronous and take a while to complete, am I blocking the event loop? I think what I'd like to happen is for each call to happen in the next pass through the event loop.
Each iteration would queue a job that includes the continuation of the await, which includes all future iterations. So all jobs queued between when one iteration is schedules vs when it executes, scheduling the next iteration, would be interleaved between the iterations. So IIUC this already does exactly what you want.
On Fri, Jul 17, 2015 at 12:57 PM, Ben Newman <benjamin at cs.stanford.edu>
wrote:
Yep, all current implementations that I know of (Traceur, Babel/Regenerator, various other generator-based approaches) do what you want, because Promise.resolve(fn()).then(result => ...) always runs the callback a future turn of the event loop.
Please note I might not be using the term "turn" precisely here (microtask?); my point is just that your example would not be blocking the event loop, and that's a good thing!
"task" and "microtask" are browser concepts, not JS concepts. The JS concept is indeed what E calls a "turn", but the official EcmaScript standards language for turn is "job". Please let's stick with "job".
On Fri, Jul 17, 2015 at 3:06 PM, Alexander Jones <alex at weej.com> wrote:
Changing a function from returning a Promise<T> to returning a T is just the same as changing it to return any other type.
This sounds like the old monad vs promise debate. EcmaScript promises have already decided against the monad camp. Whether that decision is right or wrong, it is behind us. Promises are not parametrically polymorphic over all T. In particular when T is Promise<U>,
Promise<T> === Promise<Promise<U>> === Promise<U>.
Meaning putting a Promise in a chain of promises is a point of no return so I've never seen/heard/imagined a case where you have a promise and suddenly you refactor that to be synchronous.
The specific "Promise gonna Promise" was actually mentioning another thread about cancelability and the fact "Promises are Promises and should just Promise" :-)
Sorry for the confusion
Mark S. Miller wrote:
We've talked about allowing await at the top level of modules, I think so that the await continuation could proceed after the synchronous part of the load. I am unclear on the details and cannot reconstruct a sensible story from memory.
I'd love to see that, using await
in my main.js (or app.js or whatever
it is called), so that I can write scripts using asynchronous functions
without having to wrap everything in an IEAFE (immediately-executed
asynchronous function expression).
Not sure whether such syntax might also be used with asynchronous module
loaders, but at least at the top level of an app this would certainly be
useful.
While JSON.parse(await fs.readFile("options.json"))
could trivially be
replaced with fs.readFileSync
, such might not be the case for await db.readTable("config")
.
, Bergi
On Sun, Jul 19, 2015 at 10:16 AM, Bergi <a.d.bergi at web.de> wrote:
Mark S. Miller wrote:
We've talked about allowing await at the top level of modules, I think so
that the await continuation could proceed after the synchronous part of the load. I am unclear on the details and cannot reconstruct a sensible story from memory.
I'd love to see that, using
await
in my main.js (or app.js or whatever it is called), so that I can write scripts using asynchronous functions without having to wrap everything in an IEAFE (immediately-executed asynchronous function expression).
Cute. How do you pronounce it?
On Fri, Jul 17, 2015 at 6:54 PM, Andrea Giammarchi < andrea.giammarchi at gmail.com> wrote:
Meaning putting a Promise in a chain of promises is a point of no return so I've never seen/heard/imagined a case where you have a promise and suddenly you refactor that to be synchronous.
This is a great point, and I think makes a good argument against the refactoring use case. But there are other plenty of other interesting use cases (e.g. Bergi's IEAFE). Consistent sync/async resolution semantics would allow you to write generic sync/async handlers, which comes up all over the place.
This approach neatly cleans up some of the inconsistencies around handling
sync throws vs. rejections vs. zalgo throws (usually promise-returning
functions which throw before returning a promise). A try/catch around some
await
will handle all three cases the same, almost -- there's one small
catch (w/ apologies for the bad pun)...
IIUC the behavior of Promise.resolve
alone are insufficient to get
identical semantics between sync and async calls -- you'd need to spin the
event loop before rethrowing a sync exception, akin to the behavior of
bluebird's Promise.try
. A minor detail, but punting on this would force
users into wrapping every call with something like Promise.try
, which
seems like a shame. ISTM these try
semantics, rather than
Promise.resolve
, are more natural as the foundation for await
.
The specific "Promise gonna Promise" was actually mentioning another
Just a few points to be careful about:
Several people have been trying to assert in threads that await can be used to solve synchronous problems; but just like Dean stated, you need to turn the event loop which makes it problematic for things like whatwg/loader#54 need to be sure we do not get confused.
I want to make sure that any proposal does not squash the idea of Compositional Functions ( jhusain/compositional-functions ). For various reasons we do not use async/await with promises currently and use the same semantics as compositional promises due to resource management.
On 17 July 2015 at 23:39, Mark S. Miller <erights at google.com> wrote:
On Fri, Jul 17, 2015 at 10:41 AM, Andrea Giammarchi < andrea.giammarchi at gmail.com> wrote:
If I might, if there's one thing that has never particularly shone in JS, that is consistency.
I see only two possibilities here: 1) it throws with non Promises 2) it "Promisify" anything that's not a Promise as if it was a
Promise.resolve(1)
... but since there's too much magic in the second point, I'd rather stick with the first one.Definitely #2. Had #1 been proposed, async/await never would have achieved consensus.
Wait, what?? Oh no, please don't bake that sloppy craze deeper into the language! Implicit conversions are Not Good.
On Thu, Jul 23, 2015 at 9:06 AM, Andreas Rossberg <rossberg at google.com>
wrote:
On 17 July 2015 at 23:39, Mark S. Miller <erights at google.com> wrote:
On Fri, Jul 17, 2015 at 10:41 AM, Andrea Giammarchi < andrea.giammarchi at gmail.com> wrote:
If I might, if there's one thing that has never particularly shone in JS, that is consistency.
I see only two possibilities here: 1) it throws with non Promises 2) it "Promisify" anything that's not a Promise as if it was a
Promise.resolve(1)
... but since there's too much magic in the second point, I'd rather stick with the first one.Definitely #2. Had #1 been proposed, async/await never would have achieved consensus.
Wait, what?? Oh no, please don't bake that sloppy craze deeper into the language! Implicit conversions are Not Good.
Out of curiosity, can you give an example of the Not Good parts? ISTM the
await
prefix is more of an explicit cast than an implicit conversation,
and other than the very small timing gap in how throws are handled I
pointed out a few days ago, I can't think of any situations where a throw
would make more sense than casting values to promises.
Out of curiosity, can you give an example of the Not Good parts? ISTM the
await prefix is more of an explicit cast than an implicit conversation, and other than the very small timing gap in how throws are handled I pointed out a few days ago, I can't think of any situations where a throw would make more sense than casting values to promises.
Sure, lots of such cases exist today with promises and are easy to reproduce
async function foo {
let result = await bar; // instead of bar();
let result2 = await callbackFn(); // await on function that was not
"promisified yet", that is, it takes a callback
let result3 = await [p1, p2, p3]; // people might expect a Promise.all
on an array, but this doesn't actually wait for anything.
}
On Sun, Jul 26, 2015 at 8:07 AM, Benjamin Gruenbaum <benjamingr at gmail.com>
wrote:
Out of curiosity, can you give an example of the Not Good parts? ISTM the await prefix is more of an explicit cast than an implicit conversation, and other than the very small timing gap in how throws are handled I pointed out a few days ago, I can't think of any situations where a throw would make more sense than casting values to promises.
Sure, lots of such cases exist today with promises and are easy to reproduce
async function foo { let result = await bar; // instead of bar(); let result2 = await callbackFn(); // await on function that was not "promisified yet", that is, it takes a callback let result3 = await [p1, p2, p3]; // people might expect a Promise.all on an array, but this doesn't actually wait for anything. }
The first example is a basic type error, not at all specific to promises or async programming -- it could be made any number of ways. If you want to call it a footgun, that's fine -- but ISTM the only real solution is "use TypeScript" (or some analog).
I don't see how the last example could possibly exist today as it reflects
a hypothetical user's misunderstanding of async/await
syntax
specifically. Perhaps there's a case to be made that there's a footgun here
(though I'd disagree), but it's beside the point of this thread.
But the second example, await callbackFn()
, seems reasonable. I still
think it would be a shame to punt on otherwise elegant ergonomics just due
to this, but I buy that there's at least some benefit now.
I suppose it'd always be possible to introduce something like
await.resolve
if there's demand for this kind of sync/async normalization.
2015-07-27 5:42 GMT+02:00 Dean Landolt <dean at deanlandolt.com>:
On Sun, Jul 26, 2015 at 8:07 AM, Benjamin Gruenbaum <benjamingr at gmail.com> wrote:
Out of curiosity, can you give an example of the Not Good parts? ISTM the await prefix is more of an explicit cast than an implicit conversation, and other than the very small timing gap in how throws are handled I pointed out a few days ago, I can't think of any situations where a throw would make more sense than casting values to promises.
Sure, lots of such cases exist today with promises and are easy to reproduce
async function foo { let result = await bar; // instead of bar(); let result2 = await callbackFn(); // await on function that was not "promisified yet", that is, it takes a callback let result3 = await [p1, p2, p3]; // people might expect a Promise.all on an array, but this doesn't actually wait for anything. }
The first example is a basic type error, not at all specific to promises or async programming -- it could be made any number of ways. If you want to call it a footgun, that's fine -- but ISTM the only real solution is "use TypeScript" (or some analog).
I don't see how the last example could possibly exist today as it reflects a hypothetical user's misunderstanding of
async/await
syntax specifically. Perhaps there's a case to be made that there's a footgun here (though I'd disagree), but it's beside the point of this thread.
I recently found out that a popular async cflow library for node based on generators, co tj/co does allow one to "await" (yield)
an array, with the semantics that Benjamin alluded to (Promise.all synchronization). At least it indicates that this is not a totally unrealistic expectation on behalf of users. Personally I think automatically Promise.all'ing arrays is a bit too implicit, but I can understand why people find it appealing.
But the second example,
await callbackFn()
, seems reasonable. I still think it would be a shame to punt on otherwise elegant ergonomics just due to this, but I buy that there's at least some benefit now.
Speaking from experience with a programming language I was involved with
called AmbientTalk, which has JS-style Promises, we did run into this issue
and we had a pragmatic way of solving it. Translated into the context of
this discussion, what we specified is that await undefined
would raise a
runtime error, while await any-non-promise-value
would coerce the
non-promise value into a resolved promise and await that promise instead.
At first sight, it might feel awkward for await
to special-case
undefined
, but given that it is JS's implicit return value, I suspect it
would catch a lot of errors of the type Benjamin described above. It's a
pragmatic way of dodging the footgun without completely breaking the
symmetry between await and Promise.resolve.
Promises may reasonably resolve with undefined
so I don't see good reason
to special-case it.
I remain of the opinion that this is a simple type issue. You can't just take a function returning number and change it to return array-of-number and expect things to work, without refactoring the call sites.
Alex
I know the spec for this isn't finalized, but what is the current direction for the behaviour when await is used on a function that is not marked async and doesn't return a Promise? Should it run immediately or wait for the next turn of the event loop?