Await. And then? #3/4: the main differences

Photo by Chris Sabor on Unsplash

Await. And then? #3/4: the main differences

Exploring both worlds.

We saw in previous bites how async/await solves all of the issues with thenables. Now, it is time to have a look at the differences between the two approaches.

Execution models

Thenables and async/await are very different in the way they execute code.

function takeAWalk() {
  return takeAWalkAPI().then(() => console.log("walking on the beach"));
}
takeAWalk().then(() => console.log("relax"))
console.log("back home")

The output is:

$ back home
$ walking on the beach
$ relax

We have seen this already when talking about thenables chaining. Thenables have a non-blocking execution model. Calls to .then are just plain common methods invocations, so as long as takeAWalk is invoked the execution just moves on to the next line, in our case the console.log("back home").

What about async/await instead?

async function takeAWalk() {
  await takeAWalkAPI()
  console.log("walking on the beach")
}
await takeAWalk()
console.log("relax")
console.log("back home")

The output this time is:

$ walking on the beach
$ relax
$ back home

This is because async/await blocks execution until the awaited promise settles, while .then does not, as it is just a chained method. Await has a blocking execution model. This is critical. When takeAWalk is invoked, function execution is halted, saved and will be picked up and continued only when the promise resolves.

To show off an example where this matters the most, let me introduce the concept of promise dependency.

Promise dependency

I used this terminology a few times in the past bites, now it's time to spotlight it using some examples and definitions.

Independent promises are promises in which execution order does not matter. We can parallelize them.

// independent promises
// this code...
await a()
await b()
// ...is equal to this
await b()
await a()

// with thenables
a().then(() => b())
// equals to 
b().then(() => a())

// we can parallelize them safely
await Promise.all([a, b])
// or
Promise.all([a, b]).then(() => { ... })

Two promises have an explicit dependency when the result of one promise is input to the second one. This code cannot be parallelized.

// explicit promise dependency
const aRes = await a();
await b(aRes); // a output is b input

// with thenables
a().then((aRes) => b(aRes))

Two promises have an implicit dependency when there is no explicit dependency between them and, nonetheless, they cannot be parallelized. In other words, changing the order changes the overall behavior, even if it is not evident by the code itself. To spot this dependency we have to grasp a knowledge of the whole context, e.g. other files involved in the call chain, or even the server code if the promises are fetching data remotely.

// implicit promise dependency
// no explicit dependency, nonetheless this...
await a();
await b();
// ...is NOT equal to this
await b();
await a();

// with thenables
a().then(() => b())
// is NOT equal to
b().then(() => a())

While independent promises and explicit dependency are easy to grasp, the most interesting form of dependency is implicit dependency. What does it mean in real life?

In the next bite, we'll look at two cases of implicit dependency where async/await cannot help us because of its execution model.