DEV Community

Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

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 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.

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:

Conclusions

  • 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

https://logrocket.com/signup/

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.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single page apps.

Try it for free.


The post Promise chaining is dead. Long live async/await appeared first on LogRocket Blog.

Top comments (0)