Optimizing aync calls in Node JS

Optimizing aync calls in Node JS

Crysfel Villa Programming

A few months ago I was cleaning up an old node codebase, the endpoint was using promises to call several other API endpoints on the network, waiting for the response on all calls, and then building a JSON response to the client.

While I was doing the refactor and cleaning up the code, I found some optimizations I'd like to share in this post.

Improving readability

In order to improve code readability and therefore lowering the maintenance cost for this service, I was introducing async/await to the codebase, the new code was looking something like this:

async function loadDocument(req, res) {
   const { id: userId } = req.user
   const { id: docId } = req.document

  try {
    const info = await doc.getDocumentInfo(id, docId)
    const permissions = await access.getPermissions(id, docId)
    const users = await teams.getUsers(docId)
    const groups = await teams.getGroups(docId)

    res.status(200).send({
      info,
      permissions,
      users,
      groups,
    })
  } catch (error) {
    res.status(500).send(error.upstream())
  }
}

Indeed, it looks very clean and easy to read; however, there are some performance issues happening here.

Optimizing async calls

Currently, we are calling one service after the other, requesting the data in sequence. Given that we don't have any dependency on the data, it would be best to call all at once instead of waiting for each to complete. But how can we do that in Node?

In node, we have Promise.all, an utility method that allows us to fire many requests at once and it will wait for all of them to resolve in a single promise.

  const [
    info,
    permissions,
    users,
    groups
  ] = await Promise.all([
    doc.getDocumentInfo(id, docId),
    access.getPermissions(id, docId),
    teams.getUsers(docId),
    teams.getGroups(docId)
  ])

There are several things going on here, let's review each one of them to have a better understanding of the optimizations.

  1. Now we are calling all those endpoints at once, each individual call will resolve separately and when all of them are completed we will get the response on each of the defined constants.
  2. We are still using await, but now only on the single promise that Promise.all returns.
  3. We are destructuring the array of values we get from all those calls, Promise.all resolves a single promise with an array containing all the responses from each individual call.

What about errors?

But now, what happens if any of those services fail? It's a fact that at some point one of those services will be down for whatever reason, if that's the case and we get an error, then Promise.all rejects the promise with the first error it gets.

When any of the promises gets rejected, the catch block in our code will get executed, in this example, we are only returning a 500 error, but we should handle the error more gracefully by sending the status code the errored service is returning or sending a better error message.

Conclusion

Using asyc/away makes your codebase really easy to read and follow, we should use it when possible; however, it might introduce some performance issues if you are not careful enough.

Did you like this post?

If you enjoyed this post or learned something new, make sure to subscribe to our newsletter! We will let you know when a new post gets published!

Article by Crysfel Villa

I'm a Sr Software Engineer who enjoys crafting software, I've been working remotely for the last 10 years, leading projects, writing books, training teams, and mentoring new devs. I'm the tech lead of @codigcoach