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.
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)
).
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!
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.
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())
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!
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 ✅ pic.twitter.com/xb39Lsp84V
— Umar Hansa (@umaar) September 28, 2018
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to hide the scrollbar in popular web browsers by making use of modern CSS techniques, and dive into some interactive examples.
Discover 16 of the most useful React content libraries and kits, and learn how to use them in your next React app.
Choosing between TypeScript and JavaScript depends on your project’s complexity, team structure, and long-term goals.
Generate and validate UUIDs in Node.js using libraries like `uuid`, `short-uuid`, and `nanoid`, covering UUID versions and best practices.
4 Replies to "Promise chaining is dead. Long live async/await"
well written, thanks
awaits are actually converted back to yields, which in turn are converted to closures…so the garbage collector argument does not hold. Otherwise, great post, thanks!
Nice article. But, if you have to replace Promise.all with an external library.. then Promise.all is not dead.
How do we do this level of asynchronous task in await?:
const result = await Promise.all([
independentTask,
taskA.then(resultA => {
return dependentOnTaskA(resultA)
})
])
The point is that independentTask is one work flow, and task->dependentOnTaskA is another workflow. And hence, neither should be waiting on either.