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.