102 comments

[ 2.7 ms ] story [ 173 ms ] thread
I wouldn't call it necessarily a "mistake" but certainly something you would want to avoid doing if you want your code to be performant.
I actually like making this kinda mistake, because it means I can get some easy performance gains in a bugfix release!
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.
Yeah, I couldn't understand what the article was about until the end because it's so obvious that the requests would not be in parallel.
It reads like it might be this guy's first week, this is extremely obvious. Confused why this has so many upvotes.
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

Exactly, it's called AWAIT damn it.

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.

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.
Debugging those got a lot better in the new Firefox dev tools.
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.
That's true, but in my experience, they also just memorize the ".then(result =>" pattern and make exactly the same mistake.
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.
Most ergonomic IMO is:

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.

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().

It doesn't inherently require a heap allocation, but the common, non-embedded use case does.
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.
I never used async/await as I'm more comfortable with promises so yeah, I thought the first code was correct.

async/await is clearer to me after reading this article though.

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.

> Can you spot the problem with this piece of code?

Sure. There's no error handling :-)

(Also, if it's not some magic fetch, the return value needs to be json-ed.)

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).

Simple!

try { ... } catch (e) { throw “an unknown error occurred” }

;)

That's what I thought they were going for at first too!

"What's wrong with this code? It tries to make `async` look more readable by skipping the ugly try/catch block/s" ;)

Clickbait title. Beware of not reading the docs and making noob mistakes.
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.
All the code in the UI should better be fast, else the UI drowns in annoying molasses real quick.

So no, performance does matter a lot here.

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.
While the idea is correct, the proposed solution is needlessly verbose. A better solution would be to

  async function getPeople() {
    const [members, nonMembers] = await Promise.all([
      fetch("/members"),
      fetch("/non-members"),
    ]);
    return members.concat(nonMembers);
  }
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.
Or if your env supports Array#flat,

const getPeople = async () => (await Promise.all(...)).flat()

or

    async function getPeople() {
      const membersPromise=fetch("/members");
      const nonmembersPromise=fetch("/non-members");
      return [].concat(await membersPromise, await nonmembersPromise);
    }
edited to fix typo spotted by lhorie. thanks.
I expect that will execute sequentially though as opposed to the solution above it.
No, the second fetch is spawned before the first awaited. So by the time execution gets deferred both fetches are running.
the execution starts when the function is called - not when "await" is prepended to its result.
Arguably correct but possibly misleading.

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.
(comment deleted)
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 ^_^

I don't expect so.

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.

That would throw `Uncaught ReferenceError: members is not defined`. I think you meant `[].concat`
Am I the only one getting itchy fingers from the lack of normalization? This:

    async function getPeople() {
      return fetch('/people')
    }
just seems far more straightforward across the entire stack (where each entry has a `member: boolean` or `type: someEnum` field)
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.
Hack has a more readable "fix" now, which will hopefully make it to JS in due course:

  async function getPeople() {
    concurrent {
      const members = await fetch("/members");
      const nonMembers = await fetch("/non-members");
    }

    return members.concat(nonMembers);
  }
...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)

  const [foo, baz, bar] = Promise.all([getFoo(), getBar(), getBaz()])
This is much more readable and less error prone:

  concurrent {
    const foo = await getFoo();
    const bar = await getBar();
    const baz = await getBaz();
  }
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.

The way I would write the code is never covered, and I can't tell it's because I don't understand async, or I'm secretly a genius.

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?

Afaik that just runs in serial, like the original example. `await` pauses execution immediately.
Yes, but the first pause comes after both requests have been started.
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.
Yes.

> 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.

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 has the same problem, (await members) will happen first, only when that’s done will (await nonMembers) happen.
That seems different, and not like a problem at all.

My understanding is that `await` refers to the resolution of a promise, not its start.

Isn't it as sequential as the original code?
I don't believe so. Both promises are created before any are awaited.
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.

    // Given:
    const fooPromise = asyncFoo();
    const barPromise = asyncBar();

    // Then this...
    await fooPromise;
    await barPromise;

    // ...is equivalent to this:
    await Promise.all([fooPromise, barPromise]);
Now, Promise.all() is useful, but it is unrelated to when computation starts.
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 can't get it, and from my experience, this is not true.

Consider:

  const x = await foo();
  await bar(x);
How can you schedule `foo()` and `bar()` in parallel, when they have an explicit data dependency?
> 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.

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:

    fetch(first).then {
      fetch(second).then {
        doThings();
      };
    };
vs.

    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)?

See this SO answer for eg. as well: https://stackoverflow.com/a/46348155

May understanding can as well be wrong!

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.
Except the default is exactly fast and racy:

fetch(x); fetch(y);

vs

await fetch(x); await fetch(y);

I thought it was about lack of error handling.

Running them in parallel is not a solution. I would create a new API call to fetch both.

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.
The mistake is not getting the body of the response. Is it even meaningful to concat two response objects.

It's amazing what gets upvoted on HN.

Would this not also work?

    async function getPeople() {
      const members = fetch("/members");
      const nonMembers = fetch("/non-members");
    
      return (await members).concat(await nonMembers);
    }
(comment deleted)
Interesting, it actually does. I agree that's cleaner than the Promise.all() pattern.
(comment deleted)
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.
Why is this article so highly upvoted? The problem clearly isn't with Async/Await itself.
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.
Another issue: you could end up with duplicate people in the set if they are moved ‘member <-> !member‘ between each read.

What you would need is a single HTTP request that uses a DB read transaction to get the whole set of people.

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.
The clue is even in the name. What do you expect to do on the keyword "await" except stop and wait?