Deal with Promises like a Pro 😎
If you've been a JavaScript developer for a while you must have come across Promises. If not, here is a quick intro
A Promise is a proxy for a value not necessarily known when the promise is created. -- MDN
I've been dealing with promises for a while now, and I think they are a great way to represent asynchronous operations in JavaScript. As great as they are, there is still a lot of functionality they have in-built that most of us don't even know (including myself, until like a week ago).
Through this post today, I would like to explain four interesting built-in features of the Promise object.
Before we dive into it, here are the utilities I used for the demo
// configs for the createPromiseArrayFromConfig function
const allItemsWillResolve = [
{ settleAfterSeconds: 1, shouldReject: false },
{ settleAfterSeconds: 1, shouldReject: false },
];
const someItemsReject = [
{ settleAfterSeconds: 1, shouldReject: false },
{ settleAfterSeconds: 1, shouldReject: true },
{ settleAfterSeconds: 1, shouldReject: false },
];
const allItemsReject = [
{ settleAfterSeconds: 1, shouldReject: true },
{ settleAfterSeconds: 1, shouldReject: true }
];
const itemsWillResolveAtDifferentTime = [
{ settleAfterSeconds: 1, shouldReject: false },
{ settleAfterSeconds: 2, shouldReject: false },
];
// creates an array of promises from the provided config
function createPromiseArrayFromConfig(arrayOfConfigs) {
// map over the array config objects and return a new Promise for each item as per the config
return arrayOfConfigs.map(
({ settleAfterSeconds, shouldReject }, index) =>
new Promise((resolve, reject) => {
// wait "settleAfterSeconds" seconds before settling the promise
setTimeout(() => {
if (shouldReject) {
reject(`Item at ${index} index couldn't resolve! `);
} else {
resolve(`Item at ${index} index resolved fine!`);
}
}, settleAfterSeconds * 1000);
})
);
}
As you can guess from the utils, we will run these four scenarios using each of the Promise
methods and see how each one behaves.
Promise.all()
The Promise.all
takes an iterable of promises as input and then returns a single promise that'll resolve into an array of the results of the input promises.
However, the returned promise will reject even if a single promise from the input array rejects. The rejection message/error will be that of the first rejected item.
Let's see its behavior through our examples.
As we can observe in the image above,
- the
allItemsWillResolve
config resolves to an array of two string messages. - the
someItemsReject
config fails with the second promise rejection as that's the first one to fail. - the
allItemsReject
config fails with the first promise rejection as that's the first one to fail. - the
itemsWillResolveAtDifferentTime
config takes two seconds to resolve. This is because all the items start resolving at the same time, and thePromise.all
takes almost the same time to resolve as the longest promise in the array.
One point to note here is that in some environments like "codesandbox" you might see 3 seconds as output, (happened with me). This can be due to other things running in the app, leaving the
setTimeout
no chance to run later at 2 seconds. I would recommend opening the output in a new tab and see if the output is correct.
Use case
A good place to use Promise.all
would be situations like mapping over an array to do dependent asynchronous operations and then wrapping the returned array of promises with Promise.all
call.
Here is a potential use case for Promise.all
, consider a user "who wishes to zip together all their images from our platform, they don't want partial data, i.e either it's all done or count it as failed."
Here the operations are dependent on each other i.e. we only care if all the operations/promises resolve, because even if one of them is missing from zip, "our operation is incomplete". Hence, it would be better to do it using a single Promise.all
call and show the error to our user if any operation fails.
Promise.allSettled()
The Promise.allSettled
takes an iterable of promises as the input and returns a single promise that resolves after all of the given promises have either been resolved or rejected, with an array of objects that each describes the outcome of each promise using value
or reason
.
Let's see its behavior through our examples.
As we can observe in the image above,
- the
allItemsWillResolve
config resolves to an array of two objects, each having astatus
and avalue
. - the
someItemsReject
config doesn't reject this time, instead it returns an array of 3 objects, the second of which has astatus
as "rejected" andreason
aserror message
. It's worth noticing that the second item is missing the key namedvalue
. - the
allItemsReject
config returns both items withstatus
as "rejected". - the
itemsWillResolveAtDifferentTime
config takes two seconds to resolve as it works likePromise.all
.
Use case
A good use case for Promise.allSettled
, would be to show our user (from the Promise.all
example above), a dialog box of which all files couldn't be zipped by looking over the returned data and showing their individual messages. This is a much better user experience compared to the previous one, where we only showed the first error we found.
Promise.any()
The Promise.any
takes an array of promises as input and returns a single promise that resolves as soon as one of the promises in the array fulfills, with the value of the fulfilled promise.
Promise.any is only available from Node version 15 and above
Let's see its behavior through our examples.
As we can observe in the image above,
- the
allItemsWillResolve
config resolves to the first item's promise. - the
someItemsReject
config resolves to the first item's promise. - the
allItemsReject
config returns an AggregateError as all of the promises are rejected. - the
itemsWillResolveAtDifferentTime
config takes one second to resolve because out of the two promises we provided, the first one took only one second to resolve.
Use case
A good use case for Promise.any
, would be to request the same resource from multiple sources and show the first one received. Imagine, if you were to connect our customer to the first support assist, the best way to do that would be to request a connection to all of them and pick the one who responded fastest.
Promise.race
The Promise.race
takes an array of promises as input and returns a single promise that fulfills or rejects as soon as one of the promises in an array fulfills or rejects, with the value or reason from that promise.
Let's see its behavior through our examples.
As we can observe in the image above,
- the
allItemsWillResolve
config resolves to the first item's promise. - the
someItemsReject
config resolves to the first item's promise. - the
allItemsReject
config returns the error from the first promise's rejection. - the
itemsWillResolveAtDifferentTime
config takes one second to resolve because out of the two promises we provided, the first one took only one second to resolve.
Some important points
- If the iterable passed is empty, the promise returned will be forever pending.
- If the iterable contains one or more non-promise values and/or an already settled promise, then Promise.race will resolve to the first of these values found in the iterable.
Also, unlike
Promise.any()
, which returns the first fulfilled value, this method returns the first settled (fulfilled or rejected) value.
Use case
A good use case for Promise.race
, would be to set a cutoff timer for operations such that if the given operation doesn't finish in x seconds we throw an error.
// wait for "seconds" before rejecting promise (throws error)
function rejectAfter(seconds) {
return new Promise((resolve, reject) => {
setTimeout(() => reject(`Request couldn't resolve in ${seconds}`), seconds * 1000)
})
}
// this will throw an error if the request doesn't resolve in // 5 seconds
function testCutoff() {
return Promise.race([testPromise, waitFor(5)])
}
Video tutorial and example
You can also watch the [Youtube video] (https://www.youtube.com/watch?v=1Mc4cFuJ224) to see the examples in action
You can also fork and play around with the codesandbox here.
I hope you enjoyed reading this article as much as I enjoyed writing it!
For more such content, please follow me on Twitter
Until next time
Resources used
I followed MDN docs while researching for this post.