I love async/await and I cannot lie. Unfortunately, I don’t have the concentration chops to trace callback chains. And I’ve also had the nuances of exception handling in promises lead me to pain and ruin. Async/Await has been there for a while, and it’s becoming harder to find reasons to not use it. This article focuses on a few things I struggled to understand when I started using it

An Async Function Always Returns a Promise

Think of an async function as a promise generator. Any value returned by an async function gets wrapped in a Promise.resolve() — unless it’s already a promise of course. That’d just be wasteful.

const assert = require("assert");

const meaningOfLife = async () => 42;
assert.equal(meaningOfLife() instanceof Promise, true);

// empty functions return promises that resolve with undefined
async function noMeaningOfLife() {}
assert.equal(noMeaningOfLife() instanceof Promise, true);

But what happens when you await an async function that returns a Promise that return a Promise?

const assert = require("assert");

// overkill, but you get the point
async function promiseReturner() {
  return Promise.resolve(Promise.resolve(Promise.resolve(Promise.resolve(42))));
}

// we use an async IIFE to await our promise
(async function() {
  assert.equal(await promiseReturner(), 42);
})();

The await keyword here takes its job seriously. It will wait for as long it takes for all promises to resolve and return a value.

So two things:

  • Don’t return values from async functions in the following manner:

        async function() { return await somePromiseHere }
    

    This will wait for the promise first and then rewrap it in a Promise.resolve() unnecessarily.

  • Although it might be tempting to add an async to every function, avoid using it for non-asynchronous purposes. Promises resolve in the next tick and may potentially slow your code down.

Callbacks can be Confusing

If you’re new to async/await, the callback behaviour is a bit strange to comprehend. We will use the following function to run a couple of experiments. The function takes a number, adds 10 to it and gives it back after a second.

async function addTenAndReturnNumber(number) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(number + 10);
    }, 1000);
  });
}

First, let’s try to use map with our function.

(async function() {
  // notice that we have to mark the callback function as async too
  // map((number) => {...}) will throw an "Unexpected Token" error on the await keyword
  const result = [41, 42, 43].map(async (number) => {
    return await addTenAndReturnNumber(number);
  });
  console.log(result);
})();

What would you expect result to evaluate to?

If you expected 51, 52, 53, then tough luck — you weren’t paying attention in the previous section. The map callback is an async function and remember — async functions always return promises. We receive a [Promise {}, Promise {}, Promise {}]. What we could do is wait for all of these promises using await Promise.all(result) to get what we desire.

Let’s try another one.

(async function() {
  let variable;
  const result = [41, 42, 43].forEach(async (number) => {
    variable = await addTenAndReturnNumber(number);
  });
  console.log(variable);
})();

In this case, we would receive an undefined and not 53 instead. This is because when you use await pauses only the async function that is immediately surrounding it. By that time, the main thread has moved on to the console.log line which returns an undefined.

A solution would be to use a for...of loop instead. This will pause the outer IIFE and hence, result in our expected output of 53.

(async function() {
  let variable;
  for (number of [41, 42, 43]) {
    variable = await addTenAndReturnNumber(number);
  }
  console.log(variable); // 53
})();

Avoid Sequential Execution

When I was new to the async-await business, I couldn’t resist using await with every promise that I could get my hands on. My naiveness convinced me that the asynchronous nature of JavaScript would magically parallelise everything. But alas, await/await is nothing but plain old promises in a sexier dress. Consider the following piece of code, where the find function returns a promise that fetches something from a database.

async function getAnimals() {
  const cats = await Dogs.find({});
  const dogs = await Cats.find({});
  return {
    cats,
    dogs,
  };
}

These two functions would be executed one after the other, but we could parallelise them to get things done faster (if they are not dependent on each other). There are two ways to do this. The first way would be to use Promise.all

async function getAnimals() {
  const [cats, dogs] = await Promise.all([Dogs.find({}), Cats.find({})]);
  return {
    cats,
    dogs,
  };
}

We could also do this by invoking the promises first, and then awaiting their results.

async function getAnimals() {
  const dogs = Dogs.find({});
  const cats = Cats.find({});
  return {
    dogs: await dogs,
    cats: await cats,
  };
}

As a bonus tip, try to avoid awaiting operations of which the results you don’t care about to speed up response times.

Now, go forth and shine. Asynchronous glory awaits you.