All Articles

async/await vs .then() in JS Promises

Promises are at the core of asynchronous programming in JavaScript, but sometimes the patterns surrounding their use can be really confusing.

First of all, there are two main things you can do with promises: create and handle them. This post will be mainly focused on the latter: how do we effectively handle promises when we need some information now, some later, and have some behavior that depends on a promise’s return value?

After many a JavaScript coder found themselves in callback hell, ECMAScript introduced two different ways of handling promise behavior: in 2015, .then() and in 2017, await/async. Nowadays, you’re more likely to see the async/await pattern used in code.

But it turns out that despite being newer, async/await isn’t always the best tool for the task. Understanding the subtle differences from .then() – for example, await always makes things run in series, whereas .then() and Promise.all() allow for parallelism – will hopefully help you write better asynchronous code.

A refresher on .then()

Let’s start with a pretty basic promise instatiation:

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("hello")
  }, 500)
})

Similar to how one uses callback functions, you can use .then() to run code after a promise has finished executing. Then() methods can run in parallel:

myPromise.then((message) => {
  message += " world"
  console.log(message)
})

myPromise.then((message) => {
  message += " again"
  console.log(message)
})

// output:
// hello world
// hello again

They can also be chained to run sequentially so that you can operate on the same variable multiple times:

myPromise.then((message) => {
  return message += " world"
}).then((message) => {
  message += " again"
  console.log(message)
})
// output:
// hello world again

A refresher on async/await

The async/await operators work together to let you code in JavaScript in a way that looks like other synchronous code. All you need to do is write the async keyword before the function you’re using, and then the await keyword before you call the function that returns a promise.

It’s usually a good idea to wrap your await statement in a try/catch statement to catch any errors if the promise is rejected.

const threeSecondPromise = function() {
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      resolve("hello");
    }, 3000);
  });
};

async function myAsyncFunction() {
  console.log("starting execution of the function");
  try {
    let promiseMessage = await threeSecondPromise();
  } catch (errMessage) {
    console.log(errMessage);
  }
  console.log(promiseMessage);
}

myAsyncFunction()
// output:
// "starting execution of the function"
// "hello"

When you want to use the returned promise value in a subsequent operation, you can use consecutive await statements. Here, I’ve added an argument to threeSecondPromise so that it more closely mirrors the behavior of the example in the .then() section.

const threeSecondPromise = function(str) {
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      resolve(str);
    }, 3000);
  });
};

async function myAsyncFunction() {
  console.log("starting execution of the function");
  let firstPromise = await threeSecondPromise("hello");
  let secondPromise = firstPromise + await threeSecondPromise(" world");
  console.log(secondPromise)
}

myAsyncFunction()

This behavior is equivalent to if we had chained two .then() statements after each other: each await statement separately calls the threeSecondPromise() method, and the execution of the program pauses until each promise is resolved.

But sometimes you don’t want to pause the execution of the entire program while you’re waiting for your promise to resolve. What if you want to run a bunch of promises in parallel and do something once they’re all done? One way would be to write lots of non-chained .then() statements like we did in the previous section:

const firstPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("second")
  }, 500)
})

const secondPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("third")
  }, 1000)
})

const thirdPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("first")
  }, 200)
})

firstPromise.then((val) => {
  console.log(val);
})

secondPromise.then((val) => {
  console.log(val);  
})

thirdPromise.then((val) => {
  console.log(val);  
})

// output:
// first
// second
// third

Each of the .then() statements starts at the same time, and then resolves according to their respective timers. Unlike async/await, we’re not waiting around for each promise to finish before starting the next.

But this pattern is pretty ugly. There’s a better solution!

Using Promise.all()

Promise.all() takes a single iterable (usually an array of promises) argument, and if all of the promises within the iterable resolve, returns an array of their resolved values.

If we take the same promises as the above example, here’s how we’d use Promise.all():

Promise.all([firstPromise, secondPromise, thirdPromise]).then(values => { 
  console.log(values); // [ 'first', 'second', 'third' ] 
});

This is a much cleaner approach for parallel execution!

Summary

In sum:

  • use async/await if you want to execute a single promise, or multiple promises in series. In other words, if you want to wait for each promise to resolve before continuing the execution of your program.
  • use .then() if you want to start parallel execution on promises in different places in your program, or if you want to create multiple parallel instances of the same promise.
  • use Promise.all() for multiple promise execution if don’t care what order they complete in, as long as they’re running at the same time, and/or you want do something after they’re all done.

Thanks for reading!

Credit: MDN Async Reference Understanding Promises in Javascript