Photo by Ioan Sameli on Unsplash
[Promises 3/3] Working with parallel promises
This is the last of a 3-part series of posts where we learn how to deal with promise concurrency
We learned about the benefits of promise chaining in my previous post where we learned how returning a promise to its parent can make all the promises handled in one properly aligned line that results in one common thread of code to handle all the errors (if and when they occur) and it improves readability. However, they are only beneficial when all of the subsequent promises have a dependency on each other and therefore they will need to be called sequentially.
Promise Concurrency
There are times when we need to execute multiple independent asynchronous tasks before executing one specific task. Hence, these result in a dependency on one task in multiple independent tasks. This is what we will focus on today. It is an important topic as we often need to ensure a number of promises are resolved before we call another promise. However, there may not be a benefit to us by calling them sequentially
For Eg.
Imagine a case where we need to ensure promise4 is called after promise1, promise2, and promise3 are fulfilled. With promise chaining, the expected scenario can be achieved in the following way.
promise1
.then(() => promise2)
.then(() => promise3)
.then(() => promise4)
.catch(reason => handle(reason))
This method takes care of our current case as promise4 is called after promise1, promise2, and promise3 are fulfilled. However, promise2 is called only after promise1 is fulfilled. And promise3 is called only after promise2 is called. This is good in some cases where we explicitly want it but for the case where promise1, promise2, and promise3 have no dependency on each other, this seems like a waste of time and not really an efficient data flow architecture.
This is where we summon Promise.all().
Promise.all()
Look at the above example using promise.all(). We can write it simply like this.
Promise.all(
[
promise1,
promise2,
promise3
])
.then(() => promise4)
.catch(reason => handle(reason))
promise4 is called after promise1, promise2 and promise3 are called in this case as well. However, promise1, promise2, and promise3 do not wait for each other to finish to be executed. Each of the 3 promises begins in the immediate next clock and Promise.all() takes care of waiting for all of the 3 promises to be fulfilled and then proceeds with executing the .then block of code. Similarly, a rejection on any of the 3 promises would lead the code execution to the .catch block of code to handle any of the rejections at one single place. Both the methods of sequential and parallel executions push the first error to the catch block. So, there is nothing to lose but a lot of time to gain.
As for the passed values in .then(), it receives the fulfilled values from each of the promises in the ordered list past to Promise.all().
let promise1 = Promise.resolve(2);
let promise2 = "hello world!";
let promise3 = new Promise((res, rej) => setTimeout(() => res(7), 1000));
Promise.all([promise1, promise2, promise3])
.then(values => console.log(values))
// [2, "hello world!", 7]
And for the fail part,
let promise1 = Promise.reject(2);
let promise2 = "hello world!";
let promise3 = new Promise((res, rej) => setTimeout(() => res(7), 1000));
Promise.all([promise1, promise2, promise3])
.then(values => console.log(values))
.catch(value => console.log(value))
// 2
These are the uses of Promise.all().
Promise.all() is fulfilled when all its promises are fulfilled. It passes an array of their fulfilled values to its .then block and handles its first rejection from its .catch block.
This is extremely convenient for our current purposes and is used a lot in professional software as an initializer of independent services or databases. However, there are times when we are only interested in all of the promises getting settled (rejected or fulfilled) and we want values for each individual promises with their status. That is when we need Promise.allSettled().
Promise.allSettled()
This method is also useful but has fewer practical applications as more often than not, we are interested in all promises getting resolved and we want to avoid our promises getting rejected. But for learning purposes, here is an example of how it works.
let promise1 = Promise.reject(2);
let promise2 = "hello world!";
let promise3 = new Promise((res, rej) => setTimeout(() => res(7), 1000));
Promise.allSettled([promise1, promise2, promise3])
.then(values => console.log(values))
// [
// {
// status: "rejected",
// reason: 2
// },
// {
// status: "fulfilled",
// value: "hello world!"
// },
// {
// status: "fulfilled",
// value: 7
// }
// ]
As you can see, Promise.allSettled() responds with an array of all settled promises with "reason"(s) for rejected promises which are usually filled with Error objects for us to handle them later and resolved "value"s for fulfilled promises.
Promise.allSettled() is fulfilled when all its promises are settled (fulfilled/rejected). It passes an array of their fulfilled values and rejected reasons to its .then block.
So, here we looked at methods where our primary expectation is to get all the promises fulfilled or settled at the least. However, there are times when we need just about any promise that gets fulfilled in an array of promises. That is when we need Promise.any()
Promise.any()
A practical application of Promise.any() is when there are several mirrors/copies of the same data in different servers and we are only interested in the data itself and not its source. In this case, we can make fcgi GET calls to each individual server and pass an array of those promises as an argument to Promise.any(). When any of them is fulfilled, its value is passed on to the .then block of Promise.any(). It is rejected only if all of the promises in the array is rejected which results in an AggregateError in its .catch block.
Here is an example of Promise.any()
let promiseGetFromTokyo = new Promise(
// get values from servers in Tokyo
);
let promiseGetFromParis = new Promise(
// get values from servers in Paris
);
let promiseGetFromNewyork = new Promise(
// get values from servers in NewYork
);
Promise.any([promiseGetFromTokyo, promiseGetFromParis, promiseGetFromNewyork])
.then(value => console.log(value))
// response of promiseGetFromParis
Since I live in Berlin, promiseGetFromParis is most likely to be fulfilled at the earliest and hence we will have the values from that promise in our .then block.
Promise.any() is fulfilled when any of the promises is fulfilled. It passes the values of that fulfilled promise to its .then block. Promise.any() is rejected when all of the promises in its array are rejected.
There is one more useful way of handling parallel promises when we are interested in, not the earliest one to be fulfilled but the earliest one to settled(fulfilled/rejected). It is called Promise.race()
Promise.race()
A practical application stems from literally the name of this method -> race! We use this method of Promise when we need to race a clock with a promise, or what we often call timeout. Say, we need to show a loader gif that websites show when they face network issues. We can have 2 promises for this. One promise is simply a timeout that resolves after a fixed timeout to show a gif, and the other promise is our fcgi GET call. If we receive the results first, we show them on the screen. If it takes more time than our set timeout, we show the loader icon and execute any contingency that we may have planned for such a scenario.
Here is an eg. of Promise.race()
let ourTimeout = new Promise((res, rej) => setTimeout(() => rej(), 20000));
let promiseGetFromParis = new Promise(
// get values from servers in Paris
);
Promise.race([ourTimeout, promiseGetFromParis])
.then(() => showDataOnUi())
.catch(() => showErrorUi())
In this case, if the GET call from Paris succeeds within 20 sec, then we show the data as we would normally hope to. However, if the data is not received within 20 sec, we proceed with showing Error UI and begin with other contingencies.
Promise.race() is settled when any of the promises is settled. It passes the values of that promise to its .then block if it was fulfilled and the reason to its .catch block if it was rejected.
There are other practical scenarios when we use promises a little differently than we often use.
Other Scenarios
Different promises, common then
Imagine a dialog with a list of items and we need to process an extra step depending on the user's selection. However, the user's selection can either be a special api call or a regular api call.
function handleUserSelection()
{
// Get user selection id
const userSelectionId = this.selectedItem.id;
// We need to check if user selected id is part of specialHandlingList
(this.specialHandlingList.includes(userSelectionId)
? specialFcgiCall() // Special api call if required
: regularFcgiCall() // Regular api call for selection
).then(() => FinalProcessing())
}
One case promise, otherwise not
Like the above example, imagine if we only need to process an extra step based on selection. And otherwise, we simply proceed to final processing.
function handleUserSelection()
{
// Get user selection id
const userSelectionId = this.selectedItem.id;
// We need to check if user selected id is part of specialHandlingList
(this.specialHandlingList.includes(userSelectionId)
? specialFcgiCall() // Special api call if required
: Promise.resolve() // Immediately resolve otherwise
).then(() => FinalProcessing())
}
Promise.resolve() is a promise that resolves instantly with the value passed to the resolved method. Hence, there is no waiting other than what is required for that browser's handling of tasks.
One promise, different resolve
resolve inside a promise callback function is like a return. Once a promise is settled, code execution does not proceed further.
let samplePromise = new Promise((res, rej) =>
{
// check if the condition is true for value1
if (conditionForValue1)
resolve(value1);
// Since it is not, we proceed with other processing and resolve with final value
finalProcessing()
resolve(finalValue);
}
)
Notice above that if conditionForValue1 is met, we will never see the code proceeding beyond the 5th line. Hence, having multiple resolves or rejects in the same promise is not wrong.
This was all about promises. Next, we will meet with a collection of articles on Classes Objects and Methods. We will look into object-oriented programming on Javascript's more organized brother -> Typescript.