Handling multiple JavaScript promises

One of the great things about JavaScript promises is their flexibility when handling multiple promise outcomes.

For this, there are several methods available on the in-built Promise object, such as Promise.all, Promise.any, Promise.allSettled, Promise.any and Promise.race. For serial execution, it is possible to construct an asynchronous loop for processing promises using async-await syntax.

All we have to do to get started is store our multiple promises in an array and pass them in to one of the methods. For serial execution with an asynchronous loop, the promises in the array should be stored in functions (so they do not execute immediately).

Below an overview of the processing options covered in this tutorial for processing multiple promises:

Action:Accepts:Processing:
Promise.allArray of promisesPromise results are returned when either all are fulfilled or a single one is rejected.
Promise.allSettledArray of promises Promise results are returned when all are complete (either fulfilled or rejected).
Promise.anyArray of promises Returns the result of the first promise to be successfully fulfilled only.
Promise.raceArray of promises Returns the result of the first promise to complete (either fulfilled or rejected) only.
Async for of...await loopArray of promises in functionsExecutes each promise in order temporally, and returns the result of each as soon as they are fulfilled or rejected

Promise.all

With Promise.all, promises are executed in parallel and, if all are successful, the results of all promises are returned together in an array once all are complete.

For example, in the following code, the three promises are successful, each taking 700, 1500 and 500 milliseconds to complete. So the results of each are returned together after 1500 milliseconds.

const makeCoffee = new Promise(function(resolve, reject) { 
  setTimeout(function(){ resolve("☕") },700);
});
 
const fryBacon = new Promise(function(resolve, reject) { 
  setTimeout(function(){ resolve("🥓") },100); 
});
 
const fryEgg = new Promise(function(resolve, reject) { 
  setTimeout(function(){ resolve("🍳") },1000); 
});

const myPromises = [makeCoffee, fryBacon, fryEgg];

Promise.all(myPromises)
   .then(breakfast => console.log(breakfast))
   .catch(notBreakfast => console.log(notBreakfast))

// Output: ['☕', '🥓', '🍳'] (after 1000 milliseconds)

But what is one of the promises fails?

In this case, as soon as a promise fails, Promise.all will return an error and stop processing:

const makeCoffee = new Promise(function(resolve, reject) { 
  setTimeout(function(){ reject("⚠️"); resolve("☕") },700);
});
 
const fryBacon = new Promise(function(resolve, reject) { 
  setTimeout(function(){ resolve("🥓") },100); 
});
 
const fryEgg = new Promise(function(resolve, reject) { 
  setTimeout(function(){ resolve("🍳") },1000); 
});

const myPromises = [makeCoffee, fryBacon, fryEgg];

Promise.all(myPromises)
   .then(breakfast => console.log(breakfast))
   .catch(notBreakfast => console.log(notBreakfast))

// Output: ⚠️ (after 700 milliseconds)

Because of this error behavior, Promise.all is most appropriate for processing interdependent promises (e.g. our code will fail in some major way if all promises do not complete successfully).

The drawback is that nothing is returned for early-completing promises. For this, promises should probably be handled individually.

Promise.allSettled

Promise.allSettled works similarly to Promise.all, except that a rejected promise does not create an error that stops any further processing

Instead, the results of all promises (fulfilled or rejected) are returned when all are complete.

The result is returned as an array of objects, with each containing a property named status that contains the value of either 'fulfilled' or 'rejected'. The second property is named either 'value' and contains the return value of the promise or 'reason' with the error message as the value.

Here it is in action:

const makeCoffee = new Promise(function(resolve, reject) { 
  setTimeout(function(){ reject("⚠️"); resolve("☕") },700);
});
 
const fryBacon = new Promise(function(resolve, reject) { 
  setTimeout(function(){ resolve("🥓") },100); 
});
 
const fryEgg = new Promise(function(resolve, reject) { 
  setTimeout(function(){ resolve("🍳") },1000); 
});

const myPromises = [makeCoffee, fryBacon, fryEgg];

Promise.all(myPromises)
   .then(breakfast => console.log(breakfast))
   .catch(notBreakfast => console.log(notBreakfast))

// Output:
// [
//     {
//         "status": "rejected",
//         "reason": "⚠️"
//     },
//     {
//         "status": "fulfilled",
//         "value": "🥓"
//     },
//     {
//         "status": "fulfilled",
//         "value": "🍳"
//     }
// ]

Promise.allSettled is most useful when there is some interdependence between the promises, but a single rejection shouldn't be a show-stopper for your web page or app.

Promise.any

Promise.any returns only one result: the return value of the first successfully fulfilled promise in the array. The results of all other promises are ignored.

For example, in the following snippet, ☕ is returned after 700 milliseconds because it is the return value of the first promise to successfully resolve (even though fryBacon returns a result after 100 milliseconds):

const makeCoffee = new Promise(function(resolve, reject) { 
  setTimeout(function(){ resolve("☕") },700);
});
 
const fryBacon = new Promise(function(resolve, reject) { 
  setTimeout(function(){ reject("⚠️"); resolve("🥓") },100); 
});
 
const fryEgg = new Promise(function(resolve, reject) { 
  setTimeout(function(){ resolve("🍳") },1000); 
});

const myPromises = [makeCoffee, fryBacon, fryEgg];

Promise.any(myPromises)
   .then(breakfast => console.log(breakfast))
   .catch(notBreakfast => console.log(notBreakfast))

// Output: '☕' (after 700 milliseconds)

Promise.any can be useful when several HTTP requests have been made for the same data from several servers, and we would like to work with the fastest one to successfully deliver the data.

Promise.race

Promise.race works in the same way as Promise.any, except that it returns the result of the first promise to return a result, even if this is a rejection.

For example, in the following example, "⚠️" is returned after 100 milliseconds:

const makeCoffee = new Promise(function(resolve, reject) { 
  setTimeout(function(){ resolve("☕") },700);
});
 
const fryBacon = new Promise(function(resolve, reject) { 
  setTimeout(function(){ reject("⚠️"); resolve("🥓") },100); 
});
 
const fryEgg = new Promise(function(resolve, reject) { 
  setTimeout(function(){ resolve("🍳") },1000); 
});

const myPromises = [makeCoffee, fryBacon, fryEgg];

Promise.race(myPromises)
   .then(breakfast => console.log(breakfast))
   .catch(notBreakfast => console.log(notBreakfast))

// Output: '⚠️' (after 100 milliseconds)

Async for of...await loop

Finally, we can replicate the serial execution of callbacks with promises. We do this by placing each promise in the array in an anonymous function and then executing each in turn within an asynchronous loop using async-await syntax.

To use async-await syntax, the loop must be created inside an asynchronous function. The keyword await can then be used inside the loop:

// 1) Create an array of promises
const myPromises = [
    myPromise1 => new Promise(resolve => setTimeout(() => 
    {return resolve("One person")}, 
    1000)),
    myPromise2 => new Promise(resolve => setTimeout(() => 
    {return resolve("Two people")}, 
    1000)),
    myPromise3 => new Promise(resolve => setTimeout(() => 
    {return resolve("A crowd 🎉")}, 
    1000)),
  ];
 
// 2) Create an async for...await loop in an async function
async function executeMyPromises(myPromises) {
    for (let promise of myPromises) {
        console.log(await promise());
    }
};
 
// 3) Run async function
executeMyPromises(myPromises);
// ......."One person"......."Two people"......."A crowd 🎉" (....... represents 1000 milliseconds)

This is a great way to start working with the first promise to complete without waiting upon the others. But it can be less efficient than Promise.all because promises are not executed in parallel.

Summary

As we have seen in this tutorial, promises provide a great deal of flexibility in handling the results of several asynchronous processes in JavaScript.

And, once promises are stored in an array, it is easy to change the processing behavior with the in-built Promise object: just change the method name to all, allSettled, any or race. The Promise object takes care of the rest!

These processing options for promises are much more varied than for callbacks, which execute asynchronous processes only serially. If we do want to replicate the behavior of callbacks, we can use an asynchronous loop.

Note that it is also possible to combine Promise.all, Promise.allSettled, Promise.any and Promise.race with async-await syntax. Check out our guide to using async-await for an accessible how-to guide.

Leave a Reply

Your email address will not be published.