Skip to main content

Promise chaining is dead. Long live async/await

​​While async functions have been around forever, they are often left untouched. Async/await is what some may consider an outcast.
​​Why?
​​​​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:
// Using promise chaining
getIssue()
  .then(issue => getOwner(issue.ownerId))
  .then(owner => sendEmail(owner.email, 'Some text'))
Now let’s look at the same code implemented with async/await:
// Using async functions
const issue = await getIssue()
const owner = await getOwner(issue.ownerId)
await sendEmail(owner.email, 'Some text')
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:
const issue = await getIssue()
const owner = await getOwner(issue.ownerId)
await sendEmail(owner.email, `Some text #${issue.number}`) // tiny change here
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:
getIssue()
  .then(issue => {
    return getOwner(issue.ownerId)
      .then(owner => sendEmail(owner.email, `Some text #${issue.number}`))
  })
As you can see, a small adjustment requires changing a few lines of otherwise beautiful code (like getOwner(issue.ownerId)).

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:
const settings = await getSettings() // we added this
const issue = await getIssue()
const owner = await getOwner(issue.ownerId)
await sendEmail(owner.email,
  `Some text #${issue.number}. ${settings.emailFooter}`) // minor change here
How would you implement that using promise-chaining? You might see something like this:
Promise.all([getIssue(), getSettings()])
  .then(([issue, settings]) => {
    return getOwner(issue.ownerId)
      .then(owner => sendEmail(owner.email,
        `Some text #${issue.number}. ${settings.emailFooter}`))
  })
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 getIssue() and 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:
const settings = getSettings() // we don't await here
const issue = await getIssue()
const owner = await getOwner(issue.ownerId)
await sendEmail(owner.email,
  `Some text #${issue.number}. ${(await settings).emailFooter}`) // we do it here
I’ve removed the await on the right side of the settingsassignment 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.
const value = await foo() || await bar()

const value = calculateSomething(await foo(), await bar())

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?
Yup.
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:

Visual Studio Code can now convert your long chains of Promise.then()'s into async/await! 🎊 Works very well in both JavaScript and TypeScript files. .catch() is also correctly converted to try/catch ✅

Comments

Popular posts from this blog

4 Ways to Communicate Across Browser Tabs in Realtime

1. Local Storage Events You might have already used LocalStorage, which is accessible across Tabs within the same application origin. But do you know that it also supports events? You can use this feature to communicate across Browser Tabs, where other Tabs will receive the event once the storage is updated. For example, let’s say in one Tab, we execute the following JavaScript code. window.localStorage.setItem("loggedIn", "true"); The other Tabs which listen to the event will receive it, as shown below. window.addEventListener('storage', (event) => { if (event.storageArea != localStorage) return; if (event.key === 'loggedIn') { // Do something with event.newValue } }); 2. Broadcast Channel API The Broadcast Channel API allows communication between Tabs, Windows, Frames, Iframes, and  Web Workers . One Tab can create and post to a channel as follows. const channel = new BroadcastChannel('app-data'); channel.postMessage(data); And oth...

Working with Node.js streams

  Introduction Streams are one of the major features that most Node.js applications rely on, especially when handling HTTP requests, reading/writing files, and making socket communications. Streams are very predictable since we can always expect data, error, and end events when using streams. This article will teach Node developers how to use streams to efficiently handle large amounts of data. This is a typical real-world challenge faced by Node developers when they have to deal with a large data source, and it may not be feasible to process this data all at once. This article will cover the following topics: Types of streams When to adopt Node.js streams Batching Composing streams in Node.js Transforming data with transform streams Piping streams Error handling Node.js streams Types of streams The following are four main types of streams in Node.js: Readable streams: The readable stream is responsible for reading data from a source file Writable streams: The writable stream is re...

Certbot SSL configuration in ubuntu

  Introduction Let’s Encrypt is a Certificate Authority (CA) that provides an easy way to obtain and install free  TLS/SSL certificates , thereby enabling encrypted HTTPS on web servers. It simplifies the process by providing a software client, Certbot, that attempts to automate most (if not all) of the required steps. Currently, the entire process of obtaining and installing a certificate is fully automated on both Apache and Nginx. In this tutorial, you will use Certbot to obtain a free SSL certificate for Apache on Ubuntu 18.04 and set up your certificate to renew automatically. This tutorial will use a separate Apache virtual host file instead of the default configuration file.  We recommend  creating new Apache virtual host files for each domain because it helps to avoid common mistakes and maintains the default files as a fallback configuration. Prerequisites To follow this tutorial, you will need: One Ubuntu 18.04 server set up by following this  initial ...