While async functions have been around forever, they are often left untouched. Async/await is what some may consider an outcast.
A common misconception is that async/await and promises are completely different things.
Spoiler alert, they are not! Async/await is based on promises.
Just because you use promises does not mean you’re tethered to the barbarity that is promise chaining.
In this article, we will look at how async/await really makes developers’ lives easier and why you should stop using promise chaining.
Let’s take a look at promise chaining:
Now let’s look at the same code implemented with async/await:
Hmmm it does look like simple syntax sugar, right?
Like most people, I often find my code appears simple, clean, and easy to read. Other people seem to agree. But when it comes time to make changes, it’s harder to modify than expected. That’s not a great surprise.
This is exactly what happens with promise chaining.
Let’s see why.
Easy to read, easy to maintain
Imagine we need to implement a super tiny change in our previous code (e.g. we need to mention the issue number in the email content — something like
Some text #issue-number).
How would we do that? For the async/await version, that’s trivial:
The first two lines are unaffected and the third one just required a minimal change.
What about the promise chaining version? Well, let’s see.
In the last
.then() we have access to the
owner but not to the
issue reference. This is where promise chaining starts to get messy. We could try to clean it up with something like this:
As you can see, a small adjustment requires changing a few lines of otherwise beautiful code (like
Code is constantly changing
This is especially true when implementing something very new. For example, what if we need to include additional information in the email content that comes from an async call to a function called getSettings().
It might look something like:
How would you implement that using promise-chaining? You might see something like this:
But, to me, this makes for sloppy code. Every time we need a change in the requisites, we need to do too many changes in the code. Gross.
Since I didn’t want to nest the
then() calls even more and I can
getSettings()in parallel I have opted for doing a
Promise.all()and then doing some deconstructing. It’s true that this version is optimal compared to the
await version because it’s running things in parallel, it’s still a lot harder to read.
Can we optimize the
await version to make things run in parallel without sacrificing the readability of the code? Let’s see:
I’ve removed the
await on the right side of the
settings assignment and I’ve moved it to the
sendEmail() call. This way, I’m creating a promise but not waiting for it until I need the value. In the meantime, other code can run in parallel. It’s that simple!
You don’t need Promise.all() because it’s dead
I have demonstrated how you can run promises in parallel easily and effectively without using
Promise.all(). So that means it’s completely dead, right?
Well, some might argue that a perfect use case is when you have an array of values and you need to
map() it to an array of promises. For example, you have an array of file names you want to read, or an array of URLs you need to download, etc.
I would argue that those people are wrong. Instead, I would suggest using an external library to handle concurrency. For example, I would use Promise.map() from bluebird where I can set a concurrency limit. If I have to download N files, with this utility I can specify that no more than M files will be downloaded at the same time.
You can use await almost everywhere
Async/await shines when you’re trying to simplify things. Imagine how much more complex these expressions would be with promise chaining. But with async/await, they’re simple and clean.
Still not convinced?
Let’s say you’re not interested in my preference for pretty code and ease of maintenance. Instead, you require hard facts. Do they exist?
When incorporating promise chaining into their code, developers create new functions every time there’s a
then() call. This takes up more memory by itself, but also, those functions are always inside another context. So, those functions become closures and it makes garbage collection harder to do. Besides, those functions usually are anonymous functions that pollute stack traces.
Now that we are talking about stack traces: I should mention that there’s a solid proposal to implement better stack traces for async functions. This is awesome, and interestingly…
as long as the developer sticks to using only async functions and async generators, and doesn’t write Promise code by hand
…won’t work if you use promise chaining. So one more reason to always use async/await!
How to migrate
First of all (and it should be kind of obvious by now): start using async functions and stop using promise chaining.
Second, you might find Visual Studio Code super handy for this:
— Umar Hansa (@umaar) September 28, 2018
- Async/await is already widely supported. Unless you need to support IE you are fine.
- Async/await is a lot more readable and easy to maintain.
- There are also technical reasons to use only async/await.
- With Visual Studio Code and probably other IDEs you can migrate your existing promise chained code easily!
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.