Async Await: 60% of the time, it works every time
Ryan Haskell
Posted on April 27, 2019
All aboard the hype train.
Hot take: async await isn't great for everything.
When I saw people writing this:
const printF = async () => {
try {
const a = await getA()
const b = await getB(a)
const c = await getC(b)
const d = await getD(c)
const e = await getE(d)
const f = await getF(e)
console.log(f)
} catch (err) {
console.error(err)
}
}
as a replacement for this:
const printF = () =>
getA()
.then(getB)
.then(getC)
.then(getD)
.then(getE)
.then(getF)
.then(console.log)
.catch(console.error)
I thought it was a big step in the wrong direction. It added a bunch of boilerplate for little benefit. I had try-catch for years, I didn't wanna go back!
I had some serious questions for my friends at work who were only using async await:
Do we really love async await, or are we just saying that because we saw it?
When to love lamp.
The example above was actually pretty atypical. It was just to point out that we don't need async/await for everything.
It's not always "more readable", it just looks more like synchronous code.
In reality, people don't pay me to printF
. Instead, I build websites for a living, which is secretly just turning strings into other strings.
So when I get this string:
'https://www.<some-website>.com/people/ryan'
I turn it into this string:
<div>
<h1>Ryan Haskell-Glatz</h1>
<section>
<h3>Posts</h3>
<ul>
<li>Elm is neat.</li>
<li>Promises are neat.</li>
<li>Saying neat is neat.</li>
</ul>
</section>
</div>
But sometimes my strings are in a database:
// MongoDB
{
people: [
{ _id: 1, slug: 'ryan', name: 'Ryan Haskell-Glatz' },
// ... more people
],
posts: [
{ _id: 12, slug: 'elm-is-neat', title: 'Elm is neat.', author: 1 },
{ _id: 13, slug: 'promises-are-neat', title: 'Promises are neat.', author: 1 },
{ _id: 14, slug: 'saying-neat-is-neat', title: 'Saying neat is neat.', author: 1 },
// ... more posts
]
}
So my Javascript functions look more like this:
const mongoose = require('mongoose')
const getPosts = (person) =>
mongoose.model('posts')
.find({ author: person })
.select('title')
.lean()
.exec()
const getPerson = (slug) =>
mongoose.model('people')
.findOne({ slug })
.select('name')
.lean()
.exec()
.then(person => person || Promise.reject(`Couldn't find a person with slug: ${slug}`))
const getPeopleDetailPage = (req) =>
getPerson(req.params.slug)
.then(person =>
getPosts(person)
.then(posts => ({ person, posts }))
)
Making things nicer
Both getPosts
and getPerson
are fine, async await wouldn't improve anything.
Notice how I nested my .then
functions in getPeopleDetailPage
? Kinda looks like that pointy triangle callback hell stuff.
The reason I nested things was because I needed access to both person
and posts
to return them back as an object.
Let's rewrite the last function:
const getPeopleDetailPage = async (req) => {
const person = await getPerson(req.params.slug)
const posts = await getPosts(person)
return { person, posts }
}
Here, person and posts are both in scope, so I don't need to nest things.
Async await is great for functions that combine other promises together. It helps us keep things in scope so we don't have to forget )
and indent 47 times!
Maybe it is better than promises...
Upgrading things later
Let's say a new collection called "tags" shows up, and we want to include Ryan's tags on his detail page.
Here's the new database:
// MongoDB
{
people: [
{ _id: 1, slug: 'ryan', name: 'Ryan Haskell-Glatz' },
// ... more people
],
posts: [
{ _id: 12, slug: 'elm-is-neat', title: 'Elm is neat.', author: 1 },
{ _id: 13, slug: 'promises-are-neat', title: 'Promises are neat.', author: 1 },
{ _id: 14, slug: 'saying-neat-is-neat', title: 'Saying neat is neat.', author: 1 },
// ... more posts
],
tags: [
{ _id: 25, name: 'js', people: [ 1 ] },
{ _id: 26, name: 'elm', people: [ 1, 2 ] },
{ _id: 27, name: 'web', people: [ 1, 5 ] },
// ... more tags
]
}
And our new getTags
function:
const getTags = (person) =>
mongoose.model('tags')
.find({ people: person })
.select('name')
.lean()
.exec()
We can update our function with Promise.all
to do some great stuff:
const getPeopleDetailPage = async (req) => {
const person = await getPerson(req.params.slug)
const [ posts, tags ] = await Promise.all([
getPosts(person),
getTags(person)
])
return { person, posts, tags }
}
Using Promise.all
will handle doing things in parallel, so we get the awesome performance and error handling benefits.
Handling Errors
Outside of this function, our users can decide how they want to handle errors.
If this was an API endpoint with ExpressJS, this is what that might look like:
const express = require('express')
const app = express()
app.get('/api/people/:slug', (req, res, next) =>
getPeopleDetailPage(req)
.then(data => res.json(data))
.catch(err => next(err))
)
Notice I used async/await without try-catch, hooray!
That's it!
Hope you enjoyed reading, I'm glad I finally came around to using async await, and I wanted to share the benefit of using it.
It's not a silver bullet for everything, but it works great with Promises.
Async await: 60% of the time, it works every time.
Posted on April 27, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.