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

How to use Ngx-Charts in Angular ?

Charts helps us to visualize large amount of data in an easy to understand and interactive way. This helps businesses to grow more by taking important decisions from the data. For example, e-commerce can have charts or reports for product sales, with various categories like product type, year, etc. In angular, we have various charting libraries to create charts.  Ngx-charts  is one of them. Check out the list of  best angular chart libraries .  In this article, we will see data visualization with ngx-charts and how to use ngx-charts in angular application ? We will see, How to install ngx-charts in angular ? Create a vertical bar chart Create a pie chart, advanced pie chart and pie chart grid Introduction ngx-charts  is an open-source and declarative charting framework for angular2+. It is maintained by  Swimlane . It is using Angular to render and animate the SVG elements with all of its binding and speed goodness and uses d3 for the excellent math functio...

Understand Angular’s forRoot and forChild

  forRoot   /   forChild   is a pattern for singleton services that most of us know from routing. Routing is actually the main use case for it and as it is not commonly used outside of it, I wouldn’t be surprised if most Angular developers haven’t given it a second thought. However, as the official Angular documentation puts it: “Understanding how  forRoot()  works to make sure a service is a singleton will inform your development at a deeper level.” So let’s go. Providers & Injectors Angular comes with a dependency injection (DI) mechanism. When a component depends on a service, you don’t manually create an instance of the service. You  inject  the service and the dependency injection system takes care of providing an instance. import { Component, OnInit } from '@angular/core'; import { TestService } from 'src/app/services/test.service'; @Component({ selector: 'app-test', templateUrl: './test.component.html', styleUrls: ['./test.compon...

How to solve Puppeteer TimeoutError: Navigation timeout of 30000 ms exceeded

During the automation of multiple tasks on my job and personal projects, i decided to move on  Puppeteer  instead of the old school PhantomJS. One of the most usual problems with pages that contain a lot of content, because of the ads, images etc. is the load time, an exception is thrown (specifically the TimeoutError) after a page takes more than 30000ms (30 seconds) to load totally. To solve this problem, you will have 2 options, either to increase this timeout in the configuration or remove it at all. Personally, i prefer to remove the limit as i know that the pages that i work with will end up loading someday. In this article, i'll explain you briefly 2 ways to bypass this limitation. A. Globally on the tab The option that i prefer, as i browse multiple pages in the same tab, is to remove the timeout limit on the tab that i use to browse. For example, to remove the limit you should add: await page . setDefaultNavigationTimeout ( 0 ) ;  COPY SNIPPET The setDefaultNav...