Taming Asynchronous JavaScript

Will Timpson
5 min readAug 14, 2017

--

If you’re writing JavaScript today, chances are you’re working with some asynchronous code. The traditional tool for handling async in JavaScript is the callback. Callbacks, like any tool, can be used beautifully in the right hands, but they have some significant limitations. The superficial problem with callbacks is that they can easily be used to build a deeply nested tangle. While this may make your code difficult to understand and work with, there are several other much more important considerations.

Though this post is focused on async JavaScript, you can absolutely use callbacks synchronously. When you pass a callback, you can’t know whether it will be used sync or async without looking at the appropriate docs or code. Furthermore, you have no way of knowing how many times your callback will be called or under which conditions. Thirdly, if you want to properly handle errors, you need to do it manually at every level or they will dissappear into the ether, taking your app’s stability with them.

Fortunately, we have some new tools to helps us manage all of these problems and improve our code’s readability and maintainability. In this post I’m going to work through several approaches to taming this common problem. Let’s do it!

Preparation

For this article I’m building a barebones Express app, and using a MongoDB database managed with Mongoose. While I will be using database calls in this article, the techniques are applicable to any async API you may find yourself working with. You can follow along and try out the code over on GitHub.

To get everything started I’m going to create two simple models, Category and Product:

Additionally, let’s write a simple function to return the results of our calls to the user:

The Problem

Let’s say that you need to get some related information for a view. For this example we’ll get:

  1. The most expensive product in the database
  2. The least expensive product in the same category
  3. The sum of the prices of all of the other products in that category

We will have to make three database calls to answer these questions and each of these calls depends on the result of the previous one.

I’ve also made us three functions for generating the query options that we will need to pass into our requests in each strategy. Separating them in this way will both keep the code DRY and allow me to emphasize the structural differences between the different strategies we are going to try.

Note: I have standardized the API so that they all return an array that needs to be spread.

Use Callbacks

To get the necessary answers with callbacks, we just need to nest them:

As you’ve probably noticed, we are well on our way to callback hell and we have had to explicitly add error handling in a rather repetitive manner. Extracting the anonymous functions to eliminate some of the nesting would be a good first step, but perhaps there is a better tool for the job? As it happens, Mongoose plays nicely with promises (though the built-in promises are deprecated and I’m going to use Bluebird).

Promises

If you’re not familiar with promises, I’d suggest reading through Google’s thorough write-up. Once you feel like you’re getting the hang of it, check out this phenomenal article on promise anti-patterns, or at least look at Bluebird’s short page on the subject.

Promises allow us to easily add error handling to our queries by passing a second function or chaining on a .catch call. Though it’s less obvious, I would argue that this is the most important benefit of using promises for an operation like this. Don’t worry, we can also use them to make our code easier to read too!

Remember that we need access to the results of each asyncronous call to build our response. There are several strategies that we can use to achieve this goal. Most simply, we could nest our promise calls, just like we nested our callbacks, but I think you’ll agree that that’s suboptimal:

We could also pass the variables along to the next .then by returning an array of promises:

My preferred solution to this problem is to first define variables that are scoped outside of your promise chain and then assign the resulting values to them as you resolve each promise. Remember to always return a promise if you need to continue your chain:

That’s much better. We have made our function much easier to work with, and we have easy error handling! Catching errors with .catch will even manage errors from any of our chained .then calls. Using promises in this way you can now easily tame even the thorniest of async conundrums. In order to employ them in places where they are not provided by default Bluebird provides a slick tool that can promisify lots of popular packages.

Async/Await

If you want to take things even further, you can use the new async function syntax. Async functions are a new JavaScript feature that allow you to easily create functions that implicitly return a promise. Inside of these functions you can use the await keyword to synchronously wait for the result of a promise. This is particularly useful for our problem since we need the results of several asynchronous calls in turn before we can continue to the next one. I would recommend that you understand promises, before you dive into async/await, as it’s actually promises all the way down!

Express doesn’t need us to return anything to respond to requests, so we can easily make our route handler an async function. We just need to add the async keyword before our callback and we’re ready to rock:

Beautiful, right? While we do need to explicitly use a try...catch block, we now only need one!

As you might expect, folks are really excited about async functions. You can use them in a bunch of ways to make your code more readable and maintainable. So where can you use them? Anywhere you can use ES2015 (ES6), so long as you run your code through Babel first. They are an official part of the ES2017 spec and are fully supported in Node.js as of version 8, which will become the official LTS branch this October.

Thinking Asynchronously

For this exercise we worked our way through a series of asynchronous function calls that needed to be executed in a particular order. There are, however, many situations where you don’t need to be so specific. For example, perhaps you just needed to get all of the Products and Categories in the database or the most and least expensive products. In these cases, you can (and probably should) call in parallel. Fortunately Promise.all has you covered:

Note: Remember that you can only await inside async functions and to always handle your errors.

I hope that this article has helped to illuminate the easily-confusing world of asynchronous JavaScript. Be sure to check out the code to see all three versions in their natural environment.

--

--