DEV Community

Ryan Haskell
Ryan Haskell

Posted on • Edited on

Async Await: 60% of the time, it works every time

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

as a replacement for this:

const printF = () =>
  getA()
   .then(getB)
   .then(getC)
   .then(getD)
   .then(getE)
   .then(getF)
   .then(console.log)
   .catch(console.error)
Enter fullscreen mode Exit fullscreen mode

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:

Brick are you just looking at things in the office and saying that you love them?

Do we really love async await, or are we just saying that because we saw it?

I love lamp.

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'
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
  ]
}
Enter fullscreen mode Exit fullscreen mode

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 }))
    )
Enter fullscreen mode Exit fullscreen mode

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 }
}
Enter fullscreen mode Exit fullscreen mode

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

60% of the time, it works every time

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
  ]
}
Enter fullscreen mode Exit fullscreen mode

And our new getTags function:

const getTags = (person) =>
  mongoose.model('tags')
    .find({ people: person })
    .select('name')
    .lean()
    .exec()
Enter fullscreen mode Exit fullscreen mode

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 }
}
Enter fullscreen mode Exit fullscreen mode

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))
)
Enter fullscreen mode Exit fullscreen mode

Notice I used async/await without try-catch, hooray!

I'm not even mad. That's amazing.

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.

That doesn't make sense.

Top comments (3)

Collapse
 
cwspear profile image
Cameron Spear • Edited

One of the biggest boons for async/await over .then is working with if statements and such:

async someFn(param) {
  const data = await getData();

  if (param) {
    data.other = await getOtherData(data);
  }

  return data;
}

That's much nicer than the alternative, which I'm not gonna type out cuz it was hard enough doing that much from my phone. But it is.

Especially if the first functions wasn't async and you had only a conditional async fn.

This isn't the greatest example, but there is a lot of stuff like this that gets much less messy with async/await. Especially if you are adding it after the fact, etc.

Collapse
 
ryan-haskell profile image
Ryan Haskell

Haha, that makes sense! And weirdly enough, I started this post on my phone and quickly switched over to my laptop. 😂

Collapse
 
andersjr1984 profile image
andersjr1984

Promise.all is pretty powerful! Great post.