The dangers of async/await

Christopher Kade - Jan 16 '20 - - Dev Community

After a few months consulting on the rewriting of a large-scale application, I've come to realize that async/await was used de facto for most asynchronous operation and parallel executions seemed to be out of the picture. For example, consider this Vue code snippet:

 async initStore(query) {
    await this.getConfig();
    await this.getUser();
    await this.checkRussianContext(query);

    await this.getBasket(this.$store.state.config.selectedCurrency),

    await this.$store.dispatch('options/fetchOptions', {
      basket : this.$store.state.basket,
    });
 },
Enter fullscreen mode Exit fullscreen mode

Here, each line of code is executed when its predecessor is completed. Meaning getUser will wait for getConfig to finish fetching data before being executed.

Here are a few points that come to mind when seeing this snippet:

  • What if one line does not need data from the previous one? Why block its execution and slow down our application?
  • Could we run unrelated methods in parallel using something like Promise.all?
  • Related methods should probably be using a then block to avoid blocking the rest of the method

The point this article will be to help you catch this code smell by showing you that using async/await by default in some cases can have a drastic impact on performance and UX.

Unrelated queries should be executed in parallel

Let's see some concrete data, shall we?

Here's the code snippet we'll be analyzing:

const getUserData = async () => {
  // Get a random dog as our user's avatar
  const res = await fetch('https://dog.ceo/api/breeds/image/random')
  const { message } = await res.json()

  // Get our user's general data
  const user = await fetch('https://randomuser.me/api/')
  const { results } = await user.json()

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Running this snippet 100 times on fast 3G (using Chrome's dev tools), the average execution time is 1231.10ms.

But why block the second query when it doesn't need the result of the first? Let's change our code to the following and re-run it 100 times.

const getUserDataFaster = async () => {
  // Execute both requests in parallel
  const [res, user] = await Promise.all([
    fetch('https://dog.ceo/api/breeds/image/random'), 
    fetch('https://randomuser.me/api/')
  ])
  const [{ message }, { results }] = await Promise.all([res.json(), user.json()])

  // ...
}
Enter fullscreen mode Exit fullscreen mode

We now have an average execution time of 612.50ms, half the time needed when both queries were executed one after the other.

The point is: if you can execute time-consuming queries in parallel, do it.

Try it out yourself on this codepen.

Unrelated code should not have to wait

Let's take my first example but with a twist:

 async initStore(query) {
   await Promise.all([
     this.getConfig(),
     this.getUser(),
     this.checkRussianContext(query)
   ])

   await this.getBasket(this.$store.state.config.selectedCurrency),

   await this.$store.dispatch('options/fetchOptions', {
     basket : this.$store.state.basket,
   });

   await initBooking()
 },
Enter fullscreen mode Exit fullscreen mode

Here, the first 3 requests are executed in parallel, whereas the next ones rely on data fetched beforehand and will therefore be executed afterwards. Although this snippet poses a problem, did you spot it?

Poor little initBooking will have to wait for both getBasket and fetchOptions to finish before executing even though it has nothing to do with the data they'll fetch.

An easy solution is to trade the await with a simple then block.

  async initStore(query) {
    await Promise.all([
      this.getConfig(),
      this.getUser(),
      this.checkRussianContext(query)
    ])

    this.getBasket(this.$store.state.config.selectedCurrency).then(async () => {
      await this.$store.dispatch('options/fetchOptions', {
        basket : this.$store.state.basket,
      });
    })   

   await initBooking()
 },
Enter fullscreen mode Exit fullscreen mode

This way, both getBasket and initBooking will be executed alongside one another.

Want to see it for yourself? Check out this codepen illustrating my example.

I'll stop the article there so I don't overload you with examples, but you should get the gist of it by now.

async/await are wonderful additions to the Javascript language but I hope you'll now ask yourself if they have their place in the specific method you're working on and more importantly: if some of your queries could be executed in parallel.

Thank you for reading, I'd love it if you gave me a follow on Twitter @christo_kade, this way we'll get to share our mutual skepticism towards awaits ❤️

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .