Await. And then? #2/4: the async/await brothers

Await, we were waiting for you!

In the previous bite, we detected three major issues with .then, here they are:

  • return statements are easy to forget, breaking promise chains

  • .then calls can lead to callback hell

  • even if we pay attention to the aforementioned points, the symmetry of .catch/catch and .finally/finally is unnatural

Let's see how async/await helps us fix those problems.

Broken promise chains

Take this sample code from the previous bite. The promise chain is broken as we missed a return statement.

function goToGym() {
    doWorkout().then(() => console.log("I finished my workout!"))
}

goToGym().then(() => console.log("Back home"))

With async/await it changes to:

async function goToGym() {
  await doWorkout()
  console.log("I finished my workout!")
}

await goToGym()
console.log("Back home")

An async function must always return a promise and when a value is returned it will always be wrapped in a resolved promise. When there is no explicit return value, then the value undefined is assumed and the promise is resolved with it. In short, the code above is equivalent to:

async function goToGym() {
  await doWorkout()
  console.log("I finished my workout!")
  return undefined
}

await goToGym()
console.log("Back home")

So there is no chance we can break our promise chain! Neat!

Nested callbacks

Take this example from the previous bite.

function giveMeFruit() {
  const fruit = []
  return fetchBananas()
    .then(bananas => {
      fruit.push(...bananas)
      // supposing we need bananas to fetch apples
      return fetchApples(bananas)
    })
    .then(apples => {
      fruit.push(...apples)
      // supposing we need apples to fetch pineapples
      return fetchPinapples(apples)
    })
    .then(pineapples => {
      fruit.push(...pineapples)
      return fruit 
    })
}

giveMeFruit().then(fruit => console.log("My fruit!", fruit));

We cannot take advantage of Promise.all, as there is a cascade dependency between promises. So let's see how async/await shuffles the card on the table.

function giveMeFruit() {
  const bananas = await fetchBananas();
  const apples = await fetchApples(bananas);
  const pineapples = await fetchPinapples(apples);
}

const fruit = await giveMeFruit();
console.log("My fruit!", fruit);

That's a lot of improvement in terms of nesting. First, every time we need to add a new fetch, we just add a line. Secondly, we got rid of that fruit array.

Error handling symmetry

And what about error handling?

function giveMeFruit() {
  const bananas = await fetchBananas();
  const apples = await fetchApples(bananas);
  const pineapples = await fetchPinapples(apples);
}

try {
  const fruit = await giveMeFruit();
  console.log("My fruit!", fruit);
} catch (err) {
  console.error(`BOOM`, err);
}

This is super neat! We also got rid of the .catch/catch and .finally/finally symmetry, now using solely the standard JavaScript.

Conclusions

Let's briefly recap the advantages of async/await over .then

  • No room for broken promise chains

  • No more nesting

  • Use of standard JavaScript syntax for catch and finally

At this point, it seems quite reasonable to always use async/await everywhere in every situation. Unfortunately, this is only 99% true.

In the next bite, we will better highlight the difference between the two approaches and pave the way to demonstrate how .then still covers cases that async/await don't.