Deal with Promises like a Pro 😎

#javascript#webdev#beginners#tutorial
cover image

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.

output of withAll

As we can observe in the image above,

  1. the allItemsWillResolve config resolves to an array of two string messages.
  2. the someItemsReject config fails with the second promise rejection as that's the first one to fail.
  3. the allItemsReject config fails with the first promise rejection as that's the first one to fail.
  4. the itemsWillResolveAtDifferentTime config takes two seconds to resolve. This is because all the items start resolving at the same time, and the Promise.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.

output from withAllSettled

As we can observe in the image above,

  1. the allItemsWillResolve config resolves to an array of two objects, each having a status and a value.
  2. the someItemsReject config doesn't reject this time, instead it returns an array of 3 objects, the second of which has a status as "rejected" and reason as error message. It's worth noticing that the second item is missing the key named value.
  3. the allItemsReject config returns both items with status as "rejected".
  4. the itemsWillResolveAtDifferentTime config takes two seconds to resolve as it works like Promise.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.

output from withAny

As we can observe in the image above,

  1. the allItemsWillResolve config resolves to the first item's promise.
  2. the someItemsReject config resolves to the first item's promise.
  3. the allItemsReject config returns an AggregateError as all of the promises are rejected.
  4. 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.

output from withRace

As we can observe in the image above,

  1. the allItemsWillResolve config resolves to the first item's promise.
  2. the someItemsReject config resolves to the first item's promise.
  3. the allItemsReject config returns the error from the first promise's rejection.
  4. 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

  1. If the iterable passed is empty, the promise returned will be forever pending.
  2. 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.

captain-america waving offReturn to All Articles