I think people miss the wait part of await and hence forget they can separate the creation and consumption of a promise. Should have been called waitFor!
As a non-JS person, I thought it was obvious what await does, but now I'm second guessing myself. What does it do that's not the "wait" part? Is it not simply causing a normally asynchronous function to block?
Edit: Sorry for the late edit, but: Is it because users are using it thinking it just "unwraps" a promise into its resulting value? I can begin to understand that, but it seems so incidental to the primary concept of "waiting" for something asynchronous to return.
Curious - did anyone look at the first example and not immediately see that the requests would occur in series? IMO the “await” actually makes this much more explicit.
Yes - I thought that perhaps "await" did something different in JS than C#, but that is literally its entire purpose: to synchronously wait for a call within an asynchronous method.
Even Wikipedia says the async/await pattern "allows an asynchronous, non-blocking function to be structured in a way similar to an ordinary synchronous function."
Honestly not sure how this made it to even 20 points.
Completely. I had no idea what the "problem" was because it was so obvious that the intention of the code was to have them run sequentially.
Next up: exception handling doesn't work if you never catch exceptions! Followed by, assigning a pointer to another variable doesn't assign the value the pointer points to! :P
What do you find painful about it? Have you tried the new vscode javascript debugger? It makes things fairly easy - I'd say the main issue is loss of local variables in async stack frames (which makes sense, unless they're closed they're gone), but that's not typically too big of a concern IMO.
Absolutely. I don't even write JS and I couldn't understand what point he was trying to make until he said it: That it was somehow not obvious that `await`... awaits?
I think many newer developers, especially ones who are naive to asynchronous programming, may learn/be taught that "when you want to get a result from an API, you just need to put 'await' in front of it", without fully understanding what exactly it entails. There may be fewer people in that situation on HN than there are in the developer population at large, but I theorize that it's a widespread issue and I wanted to shed some light on it.
But I maintain that in the illustrated case - where you want to get all the different independent values and then combine them at the end - it's actually less ergonomic to do it the wrong way with .then(...) than it is to do it the right way.
I did have this misunderstanding when relearning Promises and Async/Await in JS. The misunderstanding is actually not about await but about when a Promise is executed. The question you ask yourself when learning Promises is why calling await on a promise directly vs calling it on a variable containing a promise is different. Of course the root cause of the misunderstanding is not understanding Promises are executed immediately and await blocks on a promise. So creating a promise after an await causes the second promise to be executed after the awaited promise is resolved.
For me I still had my mental model from a lazy execution environment (Dask) and that’s where my misconception came from.
Thanks for the contrarian perspective. Agree that if ones mental model of JS execution is flawed there will be confusion, but I’m not sure that is really reason to “beware of async/await”.
It's a common problem with many languages - I've experienced it recently myself with Rust's futures. It requires you to be explicit about spawning tasks since that requires a heap allocation and it took me a while to figure out that I had to explicitly include an async runtime (of which there are several) and start up the async tasks myself.
That said, I agree with the GP. I immediately saw that it was going to execute sequentially and thought nothing of it. Most of the time I use async/await not for concurrency but for syntax sugar and to get proper exceptions from promise based APIs instead of the GOTO FAIL control flow breaking nonsense that is catch().
Yeah I was glossing over the specifics for this context. The way futures/generators are implemented as syntax sugar over a closure/state machine that can live on the stack is one of my favorite features of Rust, but i.e. an executor that uses a smallvec task queue is a bit esoteric for a discussion that started around Javascript.
It was immediately clear that it was sequential. Given that the name of the function is “getPeople,” I figured that was probably the mistake the author was pointing out, but it was quite obvious. And I’m not really a JavaScript developer.
This is a fairly common mistake, but it’s also easy to spot.
There doesn't need to be, if you expect the caller (or the top-level code) to handle errors.
Also, how should the function handle errors? It's not immediately obvious. An empty array is not the correct thing to return (an empty array could be a valid return value). Returning undefined/null would be another option, but that hides the error from the caller (which might be interested in the exact error message, to be able to properly deal with it).
I hate that await has made it easy to turn JS in to a synchronous language. I see so much front end code that would be better for users without await, but lots of developers prefer to write code that's easier to think about rather than fast. It's hugely annoying.
But very little code has to be fast, right? Best to prioritize making code "easier to think about" and thus more likely correct, then to profile and optimize the bottlenecks.
I don't dispute that UI code needs to be fast overall, but it's still the case that only a small proportion of the code needs to be fast in order for the whole thing to be fast.
FWIW I worked in the search/database space for a decade and it's the same thing there. There was one time I wrote a lock-free hash table implementation for a registry that needed to be optimized for concurrent access; that code was much harder to write and to understand than the rest of the codebase, but it was justified. Most of the codebase, though, was not performance-critical.
EDIT: I will grant that you still need to think about how to architect the project so that it is optimizable later, which can mean thinking about what needs to be async early on.
Not really. Front end devs should be aiming for a solid 60fps, so you only have about 16ms to do everything you need to do in a frame (less actually because the browser itself needs time as well). If you have interactions, network requests, animation, and calculations all blocking the main thread it'll be a horrible experience for the user. Giving up async and not letting the browser do what it can as soon as possible is just silly.
I feel like there's a disconnect here. I did not suggest "giving up async"; I expect that async will be used when justified — as I described having done myself in a nearby comment.
I'm just making a bog-standard argument against premature optimization, and I'm shocked that we can't find common ground.
I think sync should always have been the default. Most of the syntax ugliness and screwing-about over the years in JS and especially Node codebases that I’ve seen has been everyone wrestling against that default because they do need their code to be locally synchronous. That it’s technically async at the event loop is mostly something programmers shouldn’t usually need to worry about unless they need to leverage that.
The gist of the article is very true though. This is something that comes up every now and then in code review, even with seasoned developers. I'm not entirely sure what the solution would be since depending on the context, both sequential and parallel execution can be correct. It might just be one of those rite of passage type of mistakes you have to make once or twice.
The asynchronous portion of the work is enqueued when the function is called, but does not start executing until it's scheduled, which can't happen until the current task ends or is blocked by an await. This is useful, as it means a promise can't settle before you've had a chance to add handlers; otherwise you might sometimes get UnhandledPromiseRejections because of an unavoidable race condition.
I said arguably correct because there's nothing stopping the function from doing a portion of the work synchronously, which would mean the work as a whole starts with the function call.
Judging from your comment history, are you perhaps used to writing asynchronous code in Rust? In most languages, `Futures` start executing from when they are spawned. This is opposed to Rust, where they execute when they are `.await`ed.
Ok, 3rd edit. The promises are awaited in sequence but since they are both started before the first await they are run in parallel.
Your question about Rust futures makes a ton more sense now. This only works because a promise is self-executing / not inert like a Rust Future. Thank you kind stranger! Your async-foo is strong ^_^
IIUC, work to be done for a promise is (typically) placed in the task queue when the promise is created, and will be eligible to start running the next time you get back to the event loop, which is part of what an `await` does.
I agree the example given is poor and as others have pointed out presents a race condition where a DB transaction would be better. I like the introduction to async/await from PouchDB blog: https://pouchdb.com/2015/03/05/taming-the-async-beast-with-e...
You are not supposed to await immediately on the async tasks. Let them get fired in parallel without the await. Apply await only when you need to see the resolve/reject results.
...why? Please, please, don’t add syntax to “fix” semantics that can already be elegantly expressed using existing constructs (Promise.all). Minimal-ish syntax is one of the few areas JS gets things right.
Your argument can be applied against await/async in general - it doesn’t allow anything Promises didn’t allow before.
The reason is that it’s easy to make a mistake when destructuring the Promise.all call, and the Promise.all call doesn’t scale well (here dummy example, spot the bug)
There’s always a line and it must be drawn somewhere. Async/await allow for chaining deferred execution blocks without introducing deep syntactic nesting, at the cost of 2 keywords. In my opinion the benefits of flattening the language outweigh the costs of adding the keywords.
The hack approach otoh introduces an entirely new syntax construct (tagged blocks), which declares variables that escape their initialization block (unexpected in the JS world post-let/const), and strongly leverages the existing hack execution model whereby all await’s in a single statement are executed concurrently. This execution model is incompatible with the rest of JS (hack disallows many styles of having multiple awaits in an expression, such as `await a && await b`), and at the end of the day what are the benefits? It executes a series of statements in parallel, exactly the same as Promise.all.
If you’re concerned about mixing and matching declarations, maybe try
let foo, bar, baz;
await Promise.all([
foo = getFoo(),
bar = getBar(),
baz = getBaz()])
?
Edit: realized in the above example the variables remain of promise type, which isn’t great. One could alternatively do “getFoo().then(result => foo = result)”, but I agree this isn’t great either.
Are you sure? It seems incongruous with my mental model, which is that the first `await`, when executed, immediately pauses execution, so the second is only dispatched when the first has resolved.
The second await does not start until the first await resolves the first promise, but that doesn't mean network activity for the second initiated request isn't still happening in the background.
In all examples provided, unless you want to return them as separate promises, you must await at some point. My understanding is that calling await on a promise does not prevent other Promises from being fulfilled, it just pauses execution of the rest of the code block. For example, if you performed two web requests and then awaited a setTimeout for a few seconds, your two web request promises would likely already be resolved by the time you awaited them. Which would mean that the difference between
await fetch
await fetch
concat
and
fetch
fetch
concat await
is that the fetch can actually perform asynchronous web activity in the latter example.
This is not wrong, it's just not parallel. That is, the second promise, inside `.concat()`, will only get created and scheduled after the `await members` will have resolved.
The point of `Promise.all` is to have every promise passed to it to start computing concurrently, likely in parallel if they e.g. start independent I/O operations.
This is not true, since in JS promises are dispatched as soon as they are created. So as long as you call the fetch function, it doesn't matter that you are calling `await` sequentially.
If I follow your statement, it doesn't appear correct. JS Promises are eager, and the work underlying the promise will begin executing prior to being awaited. Await is just blocking on that execution's resolution.
This is true and this is the root cause of the misunderstanding from the blog. Promises execute immediately, await blocks on a promise. Promise.all and Promise.allSettled allow you to compose promises and block on a single promise instead of multiple awaits but have nothing to do with executing a promise.
The misunderstanding in the article has to do with misunderstanding when a promise is executed. It’s easy to wonder why calling await on a promise directly vs calling it on a variable containing a promise is different. The difference is promises execute immediately on creation while await blocks on a promise. So a promise that is created after an await will be executed after that awaited promise is resolved.
I'm not super familiar with javascript, but this would be very surprising to me? The instantiation of the work should be at the issuance of the promise, after which it would be in the queue of things to be done next time control is returned to the event loop.
`await` should just return control to the event loop (ie. actually transform the remaining code into a callback). If you've issued the two promises before that happens both should be able to start operation.
As far as I know the difference here would be effectively (in very loose pseudo-code:
a = fetch(first);
b = fetch(second);
a.then { b.then { doStuff(); } };
which to my understanding should do both fetches in parallel even though the resolution is not.
Looking at a bunch of articles about "how Promise.all can be implemented" it looks like this kind of recursive resolution of the promises is one of the methods that's expected to work (if not perform terribly well in the general case)?
I expected `await X` to create the promise `X` when it starts executing, much like `cond_A || cond_B` only computes `cond_B` when `cond_A` has been computed and turned out falsy.
Else how the following code ever work correctly?
const x = await foo();
const y = await bar(x);
We cannot schedule `bar()` before we have computed its inputs.
OTOH `(await x()).concat(await y())` could speculatively compute both promises concurrently, even if it does not yet know whether the result of `x()` has a property named `concat`. I won't normally expect it, but JS is used to defy logic in many areas :-\",
no, generally `await` is the fulfilment of a promise already created. `foo()` returns a promise, and then the await triggers the ceding of control to the event loop and allows the promise to progress.
In the example you've shown, it is absolutely correct that the two promises must be sequentially awaited like that. The OP is talking about a situation where there is no dependency between x and y.
Author here: I actually tested this out after seeing someone else post the same solution, and it does run in parallel. Because the awaits don't come until after both requests have been created, the second request isn't blocked by the first.
Personally I agree that this is cleaner than the Promise.all() solution, but I also think it emphasizes just how misleading the syntax can be, given that those weighing in on this thread can't even agree what this code will do without trying it out.
I'm personally surprised that people find this misleading? I mean, in so far as any programming construct is confusing until you understand it I can see it, but fundamentally if a promise could not progress until you waited on it, it would basically become impossible to write parallel code at all -- including implementing Promise.all().
Sometimes (most of the time?) you want to run two async functions in series because the input of one function depends on the output of the other, or because you want the side effects of each to happen in the correct order. This is the whole point of promise chaining and the async/await sugar on top of it. Concurrency is hard, and I am glad that the default is something predictable but slow, rather than fast and racy.
I thought this was going to be able stale data and race conditions, which is a much more insidious problem, IMO. The trifecta of mutability, shared state, and async operations can cause some very hard to reason about bugs.
Of the potential footguns with async/await, I'd rate this as low to non-existent. This falls into the category of straightforward code which gets a task done and which hasn't been optimized to within an inch of its life.
This is what's confusing me - I have no problem with the complaint that a language feature is confusing or unfriendly to beginners, but in this particular case, why would somebody who doesn't know what await does even use it? What are they expecting it to do besides wait?
I’ll admit that I don’t know a lot about JavaScript, but it’s a bit surreal coming from a Scala background and reading this blog. Like, I literally couldn’t understand how someone could make this mistake, until I remembered that there’re no types in JS. On that same note, Rust takes things even one step further and adds a “lifetime” axis, which if you’re used to then a similar blog post about memory access issues would be met with equal confusion about the author’s confusion. Hope that made sense.
I'm not a JS programmer so the intended "simultaneous" waits only vaguely registered as possibly problematic.
However, after reading the article it is, at worst, a performance problem.
...and performance should come after the code is correct and I don't think the code is even semantically correct in the first place.
I'm assuming that "members" and "non-members" are distinct subsets of all People. i.e. it's not possible to be in the "members" and "non-members" sets at the same time. Additionally, everyone is in one or the other.
Because the set membership is queried with 2 independent queries (designed in a reasonable and common RESTful manner) there is a race condition where an object (that changes in the time between the two calls) might appear in neither or both of the sets.
These results are then blindly concatenated together.
If the API wants to retain its RESTful design, it must include metadata about the consistency between calls. For example some kind of token that can be compared to check that the replies to the two queries represent a consistent view of the data.
If the tokens, differ, the "transaction" can be retried.
Alternatively, the API can be designed, possibly making it less RESTful in the process, so that the transaction is implemented on the server.
There are many other ways to guarantee a correct and consistent result.
Either change has big enough implications that optimising for performance at this stage would be premature because of the amount of refactoring necessary to implement `getPeople` in a way that provides consistent and correct semantics.
You make a valid point, which I probably should have mentioned, that this doesn't apply for stateful calls. But in my experience it's a common use-case (at least on the front-end) to grab two or more independent datasets, from stateless endpoints, at the same time, for display purposes. And putting those into a sequence can have a dramatic impact on perceived performance. I don't think a performance factor of 2x or more is a micro-optimization that one should hold off on making until a later pass.
The interesting point about this example is that an intelligent js compiler could easily rewrite this code into parallel execution. Its possible because the low-level await statement is declaring to wait on the execution but the calculated value is used much later first.
Maybe someone could built this for typescript, the strict typesystem should make this easily (?) solvable by some hidden wrapper type, like AwaitedButUnaccesedPromise.
The problem is it may be intentional to have the requests be sequential depending on what you're doing. It would be straightforward to identify this kind of code, but the compiler wouldn't be able to tell which was the correct business logic.
Yes, as another commenter pointed out, this would get you into trouble if the endpoint you're calling is stateful at all. And that's definitely not something a compiler could ever reason about.
Oh thats true, didnt think about it. But this problem could be easily solved by adding a „defer“ keyword which is basically await under the hood with the added compiler optimizations.
102 comments
[ 2.7 ms ] story [ 173 ms ] threadEdit: Sorry for the late edit, but: Is it because users are using it thinking it just "unwraps" a promise into its resulting value? I can begin to understand that, but it seems so incidental to the primary concept of "waiting" for something asynchronous to return.
Even Wikipedia says the async/await pattern "allows an asynchronous, non-blocking function to be structured in a way similar to an ordinary synchronous function."
Honestly not sure how this made it to even 20 points.
Next up: exception handling doesn't work if you never catch exceptions! Followed by, assigning a pointer to another variable doesn't assign the value the pointer points to! :P
It's also behaves like synchronous I/O, which is the whole point of the notation.
Which isn't to say all is rosy in async/await world. Debugging them (in JS in particular) is particularly painful.
const getPeople = async () => (await Promise.all(...)).flat()
Without await this would be
const getPeople = () => Promise.all(...).then(arrs => arrs.flat())
The difference is slight to be sure, but I usually prefer the await approach.
For me I still had my mental model from a lazy execution environment (Dask) and that’s where my misconception came from.
That said, I agree with the GP. I immediately saw that it was going to execute sequentially and thought nothing of it. Most of the time I use async/await not for concurrency but for syntax sugar and to get proper exceptions from promise based APIs instead of the GOTO FAIL control flow breaking nonsense that is catch().
async/await is clearer to me after reading this article though.
This is a fairly common mistake, but it’s also easy to spot.
Sure. There's no error handling :-)
(Also, if it's not some magic fetch, the return value needs to be json-ed.)
Also, how should the function handle errors? It's not immediately obvious. An empty array is not the correct thing to return (an empty array could be a valid return value). Returning undefined/null would be another option, but that hides the error from the caller (which might be interested in the exact error message, to be able to properly deal with it).
try { ... } catch (e) { throw “an unknown error occurred” }
;)
"What's wrong with this code? It tries to make `async` look more readable by skipping the ugly try/catch block/s" ;)
So no, performance does matter a lot here.
FWIW I worked in the search/database space for a decade and it's the same thing there. There was one time I wrote a lock-free hash table implementation for a registry that needed to be optimized for concurrent access; that code was much harder to write and to understand than the rest of the codebase, but it was justified. Most of the codebase, though, was not performance-critical.
EDIT: I will grant that you still need to think about how to architect the project so that it is optimizable later, which can mean thinking about what needs to be async early on.
I'm just making a bog-standard argument against premature optimization, and I'm shocked that we can't find common ground.
const getPeople = async () => (await Promise.all(...)).flat()
The asynchronous portion of the work is enqueued when the function is called, but does not start executing until it's scheduled, which can't happen until the current task ends or is blocked by an await. This is useful, as it means a promise can't settle before you've had a chance to add handlers; otherwise you might sometimes get UnhandledPromiseRejections because of an unavoidable race condition.
I said arguably correct because there's nothing stopping the function from doing a portion of the work synchronously, which would mean the work as a whole starts with the function call.
Your question about Rust futures makes a ton more sense now. This only works because a promise is self-executing / not inert like a Rust Future. Thank you kind stranger! Your async-foo is strong ^_^
IIUC, work to be done for a promise is (typically) placed in the task queue when the promise is created, and will be eligible to start running the next time you get back to the event loop, which is part of what an `await` does.
And their discussion of Promises: https://pouchdb.com/2015/05/18/we-have-a-problem-with-promis...
The reason is that it’s easy to make a mistake when destructuring the Promise.all call, and the Promise.all call doesn’t scale well (here dummy example, spot the bug)
This is much more readable and less error prone:The hack approach otoh introduces an entirely new syntax construct (tagged blocks), which declares variables that escape their initialization block (unexpected in the JS world post-let/const), and strongly leverages the existing hack execution model whereby all await’s in a single statement are executed concurrently. This execution model is incompatible with the rest of JS (hack disallows many styles of having multiple awaits in an expression, such as `await a && await b`), and at the end of the day what are the benefits? It executes a series of statements in parallel, exactly the same as Promise.all.
If you’re concerned about mixing and matching declarations, maybe try
let foo, bar, baz;
await Promise.all([
?Edit: realized in the above example the variables remain of promise type, which isn’t great. One could alternatively do “getFoo().then(result => foo = result)”, but I agree this isn’t great either.
The article says this is correct.
> const both = await Promise.all([ members, nonMembers ]); > return both[0].concat(both[1]);
I would have written it this way without the third promise.
> return (await members).concat(await nonMembers);
Is this wrong somehow?
> the first `await`, when executed, immediately pauses execution
Also true, but in my code, the first `await` happens after the second `fetch()` has already begun.
My understanding is that `await` refers to the resolution of a promise, not its start.
The point of `Promise.all` is to have every promise passed to it to start computing concurrently, likely in parallel if they e.g. start independent I/O operations.
The misunderstanding in the article has to do with misunderstanding when a promise is executed. It’s easy to wonder why calling await on a promise directly vs calling it on a variable containing a promise is different. The difference is promises execute immediately on creation while await blocks on a promise. So a promise that is created after an await will be executed after that awaited promise is resolved.
Consider:
How can you schedule `foo()` and `bar()` in parallel, when they have an explicit data dependency?You can't, but that's a fundamentally different scenario than what's being discussed.
`await` should just return control to the event loop (ie. actually transform the remaining code into a callback). If you've issued the two promises before that happens both should be able to start operation.
As far as I know the difference here would be effectively (in very loose pseudo-code:
vs. which to my understanding should do both fetches in parallel even though the resolution is not.Looking at a bunch of articles about "how Promise.all can be implemented" it looks like this kind of recursive resolution of the promises is one of the methods that's expected to work (if not perform terribly well in the general case)?
See this SO answer for eg. as well: https://stackoverflow.com/a/46348155
I expected `await X` to create the promise `X` when it starts executing, much like `cond_A || cond_B` only computes `cond_B` when `cond_A` has been computed and turned out falsy.
Else how the following code ever work correctly?
We cannot schedule `bar()` before we have computed its inputs.OTOH `(await x()).concat(await y())` could speculatively compute both promises concurrently, even if it does not yet know whether the result of `x()` has a property named `concat`. I won't normally expect it, but JS is used to defy logic in many areas :-\",
In the example you've shown, it is absolutely correct that the two promises must be sequentially awaited like that. The OP is talking about a situation where there is no dependency between x and y.
Personally I agree that this is cleaner than the Promise.all() solution, but I also think it emphasizes just how misleading the syntax can be, given that those weighing in on this thread can't even agree what this code will do without trying it out.
fetch(x); fetch(y);
vs
await fetch(x); await fetch(y);
Running them in parallel is not a solution. I would create a new API call to fetch both.
It's amazing what gets upvoted on HN.
However, after reading the article it is, at worst, a performance problem.
...and performance should come after the code is correct and I don't think the code is even semantically correct in the first place.
I'm assuming that "members" and "non-members" are distinct subsets of all People. i.e. it's not possible to be in the "members" and "non-members" sets at the same time. Additionally, everyone is in one or the other.
Because the set membership is queried with 2 independent queries (designed in a reasonable and common RESTful manner) there is a race condition where an object (that changes in the time between the two calls) might appear in neither or both of the sets.
These results are then blindly concatenated together.
If the API wants to retain its RESTful design, it must include metadata about the consistency between calls. For example some kind of token that can be compared to check that the replies to the two queries represent a consistent view of the data.
If the tokens, differ, the "transaction" can be retried.
Alternatively, the API can be designed, possibly making it less RESTful in the process, so that the transaction is implemented on the server.
There are many other ways to guarantee a correct and consistent result.
Either change has big enough implications that optimising for performance at this stage would be premature because of the amount of refactoring necessary to implement `getPeople` in a way that provides consistent and correct semantics.
What you would need is a single HTTP request that uses a DB read transaction to get the whole set of people.
Maybe someone could built this for typescript, the strict typesystem should make this easily (?) solvable by some hidden wrapper type, like AwaitedButUnaccesedPromise.