Running JavaScript Promises in Parallel

Promises is one of those topics that could be difficult to wrap your head around when getting started with JavaScript. However, once you grasp the concept, the next step to mastering promises is learning how to run them in parallel. The idea of running promises in parallel often comes up when you have to manage multiple unrelated promises, and this technique may be just what you need to give your website that extra speed boost. Before diving into how we can do this, we will spend some time understanding the concept of promises in sequence, in parallel, and concurrently; then, we will practice promises in parallel.

At the end of this article, you will:

  • Get a better understanding of what promises are

  • Understand the concept of sequential, parallel, and concurrent processing of promises

  • Know when to use promises sequentially or in parallel

  • Understand the concurrency methods on the promise object

  • Know how to run promises in parallel

  • Learn to make multiple requests simultaneously in parallel

What are promises in JavaScript?

If you are already very comfortable with JavaScript promises, please skip to the next heading. In JavaScript, a promise is an object that represents the initial evaluation and eventual result of an asynchronous operation in one of three states pending, fulfilled, and rejected. In any asynchronous operation, a promise will:

  • exists first in a pending state while the operation is being evaluated or processed

  • proceed to be settled in either a fulfilled state if the operation is completed successfully or a rejected state and consequently throws an error if the operation fails.

The code block below shows a practical example of how to work with promises. First, we initiate a new promise using the Promise constructor and pass a callback function to the constructor containing the code we wish to run asynchronously. The first and second parameters of this callback automatically provide us with two functions; The first function (commonly named the resolve function) takes in an argument that would be the result of the promise if the asynchronous operation is completed (fulfilled). On the other hand, the second function (commonly named the reject function) takes in an argument that would be the error thrown by the promise when the asynchronous fails to complete successfully(rejected). When working with a promise, we would commonly chain to it two types of handler methods that hold code that will run only after the promise has been settled.

The then() method

This handler is most commonly used to run code when the promise is settled in a fulfilled state. The then() method takes a 1st callback whose initial argument is the result of the fulfilled promise. After successfully verifying that the promise has been fulfilled, the then() method will execute its 1st callback. If the promise is rejected, the 1st callback will not run, and the promise will be passed on to the next handler.

Note: The then() method can also take a 2nd “error handling” callback that executes if the promise is settled in a rejected state and throws an error. However, this is not commonly used.

The catch() method

This handler is used for error handling. When the promise is settled in a rejected state and throws an error, it is usually passed down by all the chained then() methods to the catch() method to handle(except the then() method has a 2nd error handler callback). The catch() method runs code that will instruct the browser on what to do in response to the error that was thrown. This method takes just one callback function; the initial argument passed to its callback function is the error thrown by the rejected promise.

//Promises Code Example 
const myPromise = new Promise((resolve, reject) => {
  let time = Math.floor(10000 * Math.random() + 1); //'any random number between 1000-9000?
  console.log(time);
  setTimeout(() => {
    if (time < 6000) {
      resolve("request successful");
    } else {
      reject("took too long");
    }
  }, time);
});

console.log(myPromise); //Promise {<pending>}

myPromise
  .then((data) => {
    console.log(myPromise); //Promise {<fulfilled>: 'request successful'}
    console.log(data); // "request successful"
  })
  .catch((error) => {
    console.log(myPromise); //Promise {<rejected>: 'took too long'}
    console.error(error); // "took too long"
  });

In our code example, after initiating a new promise, we pass to it a callback that first generates a random number between 1000 and 9000 and then runs a setTimeout function that waits for the number of milliseconds our random number is, before executing its callback. If the random number is less than 6000, when the setTimeout function returns a fulfilled promise with a resolved value “request successful” however, if the random number is greater than 6000, the setTimeout function returns a rejected promise with an error “took too long”.

Because the setTimeout function is inherently asynchronous, it takes some time to be executed. While it is waiting for the stipulated time and is yet to be executed, the promise will exist in the pending state. However, the JavaScript processor does not wait for the promise to be settled or the setTimeout function to complete; it moves straight to the following line. Hence, when we log the value of the promise to the console, it is still pending.

As previously explained, to have a code block wait and run only after a promise is settled, we need to chain the then and catch handlers. Hence, when the random number generated is less than 6000 and the promise is fulfilled, we have the then() method executed and “request successful” logged to the console. However, if the number exceeds 6000, the promise is rejected, an error is thrown, and the catch() method handles it by logging “took too long” to the console.

Callbacks vs. Promises

A callback is a function that is passed to another function as an argument. Before the use of promises, callbacks were used to manage asynchronous code by having a function “A” take another function “B” as an argument and calling function “B” from within function “A”. This ensures that function A can only be completed after function B is completed hence stimulating asynchronous programming. However, with more complex asynchronous tasks, the callbacks get deeply nested and form an ugly, unreadable, complicated mess popularly called a “callback hell.” Promises are much preferred over callbacks in today’s JavaScript. Even more common now is the use of async/await functions to manage asynchronous tasks. However, it is noteworthy that async/await uses promises (and generators) to operate under the hood.

//Code example with callbacks 
//(This is sequential code)

const delayedColorChange = (newColor, delay, doNext) => {
    setTimeout(() => {
        document.body.style.backgroundColor = newColor;
        doNext && doNext();
    }, delay)
}
delayedColorChange('red', 1000, () => {
    delayedColorChange('orange', 1000, () => {
        delayedColorChange('yellow', 1000, () => {
            delayedColorChange('green', 1000, () => {
                delayedColorChange('blue', 1000, () => {
                    delayedColorChange('indigo', 1000, () => {
                        delayedColorChange('violet', 1000, () => {
                        })
                    })
                })
            })
        })
    })
});

The following example uses promises:

const delayedColorChange = (color, delay) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            document.body.style.backgroundColor = color;
            resolve();
        }, delay)
    })
}

delayedColorChange('red', 1000)
    .then(() => delayedColorChange('orange', 1000))
    .then(() => delayedColorChange('yellow', 1000))
    .then(() => delayedColorChange('green', 1000))
    .then(() => delayedColorChange('blue', 1000))
    .then(() => delayedColorChange('indigo', 1000))
    .then(() => delayedColorChange('violet', 1000))

//same code example but with async/await
const delayedColorChange = (color, delay) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            document.body.style.backgroundColor = color;
            resolve();
        }, delay)
    })
}
(async function(){
    await delayedColorChange('orange', 1000)
    await delayedColorChange('yellow', 1000)
    await delayedColorChange('green', 1000)
    await delayedColorChange('blue', 1000)
    await delayedColorChange('indigo', 1000)
    await delayedColorChange('violet', 1000)
})()

The code blocks above show the same asynchronous codes written using callbacks, promises, and async/await keywords. Notice how promises or async/await make our code look cleaner and more readable. Also, note the callback hell is beginning to form due to the continuously deep nesting of callbacks.

Built on Promises Apart from using the promise constructor, a couple of JavaScript keywords and APIs used in asynchronous procedures work with promises under the hood and will create and return a promise whenever we use them. A very popular example of this is the aforementioned async/await function which is currently the most popular way of writing asynchronous code in modern JavaScript. Another example of this is the browser fetch API (which is used for making AJAX requests)

const asyncToPromise=async()=>{
        await "some asynchronous task"
}
console.log(asyncToPromise()) // Promise {<pending>}
const fetchToPromise=fetch("https://images.unsplash.com/photo-1672350162337-24517666566e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=387&q=80")

console.log(fetchToPromise) //Promise {<pending>}
fetchToPromise.then(()=>{
        console.log(fetchToPromise) //Promise {<fulfilled>: Response}
    })

Notice how the fetch API and the async functions return values wrapped in a promise.

Running promises sequentially

Sequential execution of promises means that when we have multiple promises, they will be run in the order in which they appear. Each promise must wait for the completion of the previous one before beginning its execution. We will illustrate this using the code example below:

//Running Promises Sequentially
let start=Date.now()

function task1(time){
    return new Promise ((resolve,reject)=>{
        setTimeout(() => {
            resolve("task 1 completed")
            console.log(`task 1 complete in ${Date.now()-start}ms`)
        }, time);
    })
}
function task2(time){
    return new Promise ((resolve,reject)=>{
        setTimeout(() => {
            resolve("task 2 completed")
            console.log(`task 2 complete in: ${Date.now()-start}ms`)
        }, time);
    })
}
function task3(time){
    return new Promise ((resolve,reject)=>{
        setTimeout(() => {
            resolve("task 3 completed")
            console.log(`task 3 complete in: ${Date.now()-start}ms`)
        }, time);
    })
}
task1(3000)
.then(()=>task2(2000))
.then(()=>task3(1000))
//console
//task 1 complete in 3008ms
//task 2 complete in: 5027ms
//task 3 complete in: 6031ms

The code sample above has three functions (task 1, task 2, and task 3) that do the same thing. They all return a promise that waits for a given time and is fulfilled with a resolved value of “task completed”. Immediately they are resolved, the duration of time it took to process them is logged to the console.

When the three functions are executed sequentially, the order of operation will be as follows;

Task 1Task 2Task 3
Startswaitingwaiting
processingwaitingwaiting
completedStartswaiting
processingwaiting
completedStarts
processing
completed

Task 1 must be completed before task 2 can begin. And task 3 can only begin after task 2 is completed. This is how we would typically work with promises and how our promises would be executed when we use callbacks to handle asynchronous operations.

Running promises in parallel

The idea behind running promises in parallel is quite simple; here, we do not want to wait until the previous promise is settled, so we start and process multiple promises “simultaneously”.

The code block below is the same as the previous one; however, the promises are run in parallel instead.

//Running the same promise in parallel

let start=Date.now()
function task1(time){
    return new Promise ((resolve,reject)=>{
        setTimeout(() => {
            resolve("task 1 completed")
            console.log(`task 1 complete in: ${Date.now()-start}ms`)
        }, time);
    })
}
function task2(time){
    return new Promise ((resolve,reject)=>{
        setTimeout(() => {
            resolve("task 2 completed")
            console.log(`task 2 complete in: ${Date.now()-start}ms`)
        }, time);
    })
}
function task3(time){
    return new Promise ((resolve,reject)=>{
        setTimeout(() => {
            resolve("task 3 completed")
            console.log(`task 3 complete in: ${Date.now()-start}ms`)
        }, time);
    })
}
Promise.all([task1(3000),task2(2000),task3(1000)]).then((value)=>{
    console.log(value)//(3)['task 1 completed',' task 2 completed',' task 3 completed']
    })
//console
// task 3 complete in: 1011ms
// task 2 complete in: 2001ms
// task 1 complete in: 3005ms

In this case, instead of waiting for task 1 to complete its operation before task 2 gets started (as we do in sequential execution), we get started on task 1, and while task 1 is being processed, we go ahead and get started on task 2 too. The same goes for task 3. Hence the table would look more like the one below:

Task 1Task 2Task 3
Startswaitingwaiting
processingstartswaiting
processingprocessingstarts
processingprocessingprocessing
processingprocessingcompleted
processingcompleted
completed

Also, notice how task 3 is completed even before task 1 because, in parallel processing, a task only takes the amount of time required to complete itself (1000ms for task 3) since it is not waiting for any previous tasks to be completed first. The method Promise.all used in the code example is one of the few methods available on the promise object that help us run promises in parallel, and we will shed more light on it in the second half of this article.

Session Replay for Developers

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay— an open-source session replay suite for developers. It can be self-hosted in minutes, giving you complete control over your customer data

Happy debugging! Try using OpenReplay today.

Concurrent vs. Parallel

Although this technique of running multiple promises simultaneously is popularly called “running promises in parallel”, it does not occur as true parallel processing. As a matter of fact, javaScript, a single-threaded language, cannot execute anything in parallel on its own(except with the use of web workers or worker threads). A quick study of the image above explains why the term “concurrent” would be preferred and more accurate than “parallel” in describing how javascript could handle multiple promises simultaneously. Parallel processing requires multiple tasks to run simultaneously, most likely on different threads, and javascript is incapable of that.

The MDN documentation on promises clarifies this.

“Note that JavaScript is single-threaded by nature, so at a given instant, only one task will be executing, although control can shift between different promises, making execution of the promises appear concurrent. Parallel execution in JavaScript can only be achieved through worker threads.” -MDN docs on promises.

Options for running promises in parallel

The promise object provides us with four helper methods for running promises in parallel, each with its special function. They include:

  • Promise.all()

  • Promise.allSettled()

  • Promise.race()

  • Promise.any()

We will use three coding examples with different cases to demonstrate these methods.

  • Case 1 will have all promises fulfilled.

  • Case 2 will have promises 1 and 3 fulfilled and promise 2 rejected.

  • Case 3 will have all promises rejected.

//promise in parallel methods
//CASE 1: All promises are fulfilled
let start = Date.now()
function task1(time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("task 1 completed")
            console.log(`task 1 complete in ${Date.now() - start}ms`)
        }, time);
    })
}
function task2(time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("task 2 completed")
            console.log(`task 2 complete in: ${Date.now() - start}ms`)
        }, time);
    })
}
function task3(time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("task 3 completed")
            console.log(`task 3 complete in: ${Date.now() - start}ms`)
        }, time);
    })
}
Promise.all([task1(3000), task2(2000), task3(1000)])
.then((value) => console.log(value)) //['task 1 completed', 'task 2 completed', 'task 3 completed']
.catch((error)=> console.log(error))

Promise.allSettled([task1(3000), task2(2000), task3(1000)])
    .then((value) => console.log(value)) //[{status: 'fulfilled', value: 'task 1 completed'},{status: 'fulfilled', value: 'task 2 completed'},{status: 'fulfilled', value: 'task 3 completed'}]
    .catch((error)=> console.log(error))
Promise.any([task1(3000), task2(2000), task3(1000)])
    .then((value) => console.log(value))//task 3 completed
    .catch((error)=> console.log(error))
Promise.race([task1(3000), task2(2000), task3(1000)])
    .then((value) => console.log(value))//task 3 completed
    .catch((error)=> console.log(error))

The second case is as follows:

//promise in parallel methods
//CASE 2: Promises 1 and 3 are fulfilled. Promise 2 is rejected
let start = Date.now()
function task1(time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("task 1 completed")
            console.log(`task 1 complete in ${Date.now() - start}ms`)
        }, time);
    })
}
function task2(time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject("task 2 FAILED")
            console.log(`task 2 complete in: ${Date.now() - start}ms`)
        }, time);
    })
}
function task3(time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("task 3 completed")
            console.log(`task 3 complete in: ${Date.now() - start}ms`)
        }, time);
    })
}
Promise.all([task1(3000), task2(2000), task3(1000)])
.then((value) => console.log(value)) //task 2 FAILED
.catch((error)=> console.log(error))

Promise.allSettled([task1(3000), task2(2000), task3(1000)])
    .then((value) => console.log(value))//[{status: 'fulfilled', value: 'task 1 completed'},{status: 'rejected', reason: 'task 2 FAILED'},{status: 'fulfilled', value: 'task 3 completed'}]
    .catch((error)=> console.log(error))
Promise.any([task1(3000), task2(2000), task3(1000)])
    .then((value) => console.log(value))//task 3 completed
    .catch((error)=> console.log(error))
Promise.race([task1(3000), task2(2000), task3(1000)])
    .then((value) => console.log(value)) //task 3 completed
    .catch((error)=> console.log(error))

The last case is as follows:

//promise in parallel methods
    //CASE 3: All promises are rejected
    let start = Date.now()
    function task1(time) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                reject("task 1 failed")
                console.log(`task 1 complete in ${Date.now() - start}ms`)
            }, time);
        })
    }
    function task2(time) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                reject("task 2 failed")
                console.log(`task 2 complete in: ${Date.now() - start}ms`)
            }, time);
        })
    }
    function task3(time) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                reject("task 3 failed")
                console.log(`task 3 complete in: ${Date.now() - start}ms`)
            }, time);
        })
    }
    Promise.all([task1(3000), task2(2000), task3(1000)])
    .then((value) => console.log(value)) //task 3 failed
    .catch((error)=> console.log(error))

    Promise.allSettled([task1(3000), task2(2000), task3(1000)])
     .then((value) => console.log(value)) //[{status: 'rejected', reason: 'task 1 failed'},{status: 'rejected', reason: 'task 2 failed'},{status: 'rejected', reason: 'task 3 failed'}]
     .catch((error)=> console.log(error))

    Promise.any([task1(3000), task2(2000), task3(1000)])
     .then((value) => console.log(value)) //AggregateError: All promises were rejected
      .catch((error)=> console.log(error))

    Promise.race([task1(3000), task2(2000), task3(1000)])
     .then((value) => console.log(value)) //task 3 failed
      .catch((error)=> console.log(error))

Note:

  • Task 1, task 2, and task 3 are the promises passed to the promise methods’ array.

  • In all 3 cases, task 3 is completed first, task 2 second, and task 3 is completed last.

As you can observe above, all these promise methods take in an array of promises. Another thing all these promise methods have in common is that while they proceed to execute the promises within their array in parallel, they will themselves return a single promise of their own. To improve clarity, we will refer to this single promise in the rest of the article as reducedPromise. This reducedPromise, which is returned by these promise methods, is the key to understanding each promise method and what it does. The reducedPromise, like every other promise, will begin in a pending state and proceed to be settled in a fulfilled or rejected state based on different criteria for each promise method.

  • Promise.all(): Using this method, if all the promises in its array are fulfilled (Case 1), the reducedPromise it returns is also fulfilled, with its return value being an array of the return values of each of the promises that were passed into its array. This method will fail once any of the promises passed to its array is rejected, and its reducedPromise will be rejected and return the error of the first promise that failed. In case 2, once task 2 is rejected, the reducedPromise is rejected, with its error being the error of task 2. In case 3, task 3 fails first; hence the reducedPromise rejects with the error of task 3.

  • Promise.allSettled(): Here, the reducedPromise returned is fulfilled once all the promises in its array are settled. The return value is always an array of objects regardless of whether all the promises in its array are fulfilled. Each object will contain the status (fulfilled or rejected), a value with which each promise was fulfilled, and a reason for any of the rejected promises.

  • Promise.race(): The method returns a reducedPromise that is fulfilled once any of the promises in its array is settled. The promise returned has the value/error of the first promise to be settled regardless of whether it was fulfilled or rejected. In case 1, case 2, and case 3, the reducedPromise returns the resolved value of task 3 (because it is the quickest to settle).

  • Promise.any(): The method returns a reducedPromise that is fulfilled once any of the promises in its array is fulfilled. The reducedPromise return has the value/error of the first promise to be fulfilled. In cases 1 and 2, it returns task 3 since it is the quickest to be settled in a fulfilled state. If all the promises in its array are rejected (Case 3), the reducedPromise returns an aggregateError.

Use cases

There are many scenarios where it would be advantageous to use promises in parallel rather than in sequence; however, this decision boils down to a wide range of considerations. Generally speaking, using promises in sequence is optimal when the following promise depends on the result of executing the preceding promise. If the result of consequent promises is independent of each other, then parallel processing is probably the way to go, as it will be much faster. Also, thanks to the variety of methods available to us when working with promises, we can always choose the most suitable promise method for each case to run our promises in parallel.

One of the most common, practical, and important use cases of promise is in making AJAX requests. This is also a good representation of the entire promise flow because whatever server or API we are requesting will take some time to receive and process our request (pending state) and eventually responds to our request either successfully (fulfilled state) or failed (rejected state). In this section, we will look at AJAX requests as a practical example of where we could use promises in parallel.

//Case A
// const imgs=fetch("https://CompanyHeroImager")
// .then(()=>fetch("http://companyLogoImage"))
// .then(()=>fetch("https://companyTopDeal/1"))
// .then((imgs)=>{console.log("all loaded")
                    return displayPage(imgs)})
// .catch((err)=>{console.log("error:",err)})
const img1=Promise.all([fetch("https://companyTopDeal/1"),fetch("http://companyLogoImage"),fetch("https://companyTopDeal/1")])
.then((imgs)=>{console.log("all loaded")
                return displayPage(...imgs)})
.catch((err)=>{console.log("error:",err)})

The code snippet above (Case A) shows an AJAX request using the fetch API (which returns a promise) to get multiple images that will be displayed on a website. When done sequentially, if each request takes on average 3 seconds, it will take 9 seconds to fetch all the data we need and hence at least 9 seconds for our site to be displayed in full with all the required images.

However, considering that the fetching of each image is an independent event unrelated to the fetching of the next image, we should definitely go with running the promises in parallel instead. We use the promise.all method since we need all the images fetched successfully before they are loaded on our screen. Using this approach instead, site loading time for images could be reduced by up to 50% at about 3 seconds since that is the average time for loading one image.

//Case B
Promise.any([fetch("http://latestNewsFromStation1"),fetch("http://latestNewsFromStation2"),fetch("http://latestNewsFromStation3")])
.then((data)=>{console.log("latest news loaded")
            return displayLastestNews(data)})
.catch((err)=>{console.log("error:",err)})

In this second code snippet (Case B), we could imagine a situation where we have three different options on what server (or API) we could use to receive the latest news. Since we are only interested in getting the latest news, regardless of which server it comes from, we can use the promise.any method to process our request in parallel using the promise.any method we can always ensure that we get the latest news at any point in time from the server that is quickest to reply, thereby optimizing the speed of data fetching in our application.

Summary

In this article, we went over what promises are and the different ways we can conceptually go about executing them: namely sequentially and in parallel. We took a step further to differentiate parallel and concurrent execution of promises and finally had some practice working with promises in parallel using the four helper methods promise.all, promise.allSettled, promise.any, and promise.race. We established a rule of thumb that says “if the requests are totally independent of each other, parallel execution is probably a good way to go,” and rounded up by looking at some possible practical applications working with promises in parallel to manage ajax calls. Congratulations on making it to the end of this lengthy piece; hopefully at this point, you are much better at making promises. Now all you have to worry about is keeping them.

Resources

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises

For more clarity on the terminologies concerning promises, see https://github.com/domenic/promises-unwrapping/blob/master/docs/states-and-fates.md

A TIP FROM THE EDITOR: For more on promises in parallel and a new method, do check out Waiting for some promises?.