The Farrelly logo

Now that I've made too many promises, how can I keep them all

A dilemma we’ve all faced. Life is rough. Thankfully, JS provides a solution for such a situation.

This is a follow-up to the previous article It started with a Promise. If you haven’t read that, and you’re a little misty on how promises work, it’s a solid refresher. Otherwise, carry on.

Now that we’re able to make promises, access the result, and handle rejection. We’re in a stellar place to start to tackle some more complex situations we may find ourselves in.

Such as having a series to Promises to make, but not caring about the result. Or needing each Promise to resolve. The Promise API provides us four distinct methods to achieve this, Promise all, allSettled, any, and race.

All

This allows us to execute an array of promises, and returns an array of their resolved values.

The happy path, where all promises resolve

const promiseResolves = new Promise((resolve, reject) => resolve(true));

const promiseExecution = async () => {
  const promiseAll = await Promise.all([promiseResolves, promiseResolves]);
  console.log('result:', promiseAll);
};
promiseExecution();

The result is an array of the resolved results, in this case

 "result:", [true, true]

But what about when one rejects?

const promiseResolves = new Promise((resolve, reject) => resolve(true));
const promiseRejects = new Promise((resolve, reject) => reject(true));

const promiseExecution = async () => {
  const promiseAll = await Promise.all([promiseResolves, promiseRejects]);
  console.log('result:', promiseAll);
};
promiseExecution();
> Uncaught (in promise) true

We don’t see any output at all. And that’s because Promise.All will stop execution if any of the promises reject.

That leads to another problem, how can you handle a rejected promise when using Promise.All?

One option is to use a try catch. Then if a promise rejects we can handle that scenario, as an added benefit we also receive any props passed into the rejection. With this example, we receive the prop ‘false’ which is thrown above. This could easily change into being an error object, or perhaps even a callback which could handle the scenario of that specific promise failing.

const promiseResolves = new Promise((resolve, reject) => resolve(true));
const promiseRejects = new Promise((resolve, reject) => reject(false));

const promiseExecution = async () => {
  try {
    const promiseAll = await Promise.all([promiseResolves, promiseRejects]);
  } catch (e) {
    console.log('error:', e);
  }

  console.log('result:', promiseAll);
};
promiseExecution();
error: false

We could also just attach a .catch onto the end of the Promise declaration.

const promiseResolves = new Promise((resolve, reject) => resolve(true));
const promiseRejects = new Promise((resolve, reject) => reject(false));

const promiseExecution = async () => {
  const promiseAll = await Promise.all([promiseResolves, promiseRejects]).catch((e) =>
    console.log('error:', e),
  );
  console.log('result:', promiseAll);
};
promiseExecution();
"error:", false
"result:", undefined

AllSettled

‘allSettled’ is much like ‘all’, but instead allows execution to continue even if one of the promises has rejected.

This gets around having have an explicit try-catch, or ‘.catch’ handling setup.

Instead we receive an array of the promise results as an object. Each object has two properties, status, and value. Statuses are either ‘fulfilled’, or ‘rejected’. That way if we want to know the results we could map through the array and take action on for example the rejected promises.

const promiseResolves = new Promise((resolve, reject) => resolve(true));
const promiseRejects = new Promise((resolve, reject) => reject(false));

const promiseExecution = async () => {
  const promiseAll = await Promise.allSettled([
    promiseResolves,
    promiseResolves,
    promiseRejects,
    promiseRejects,
  ]);

  console.log('result:', promiseAll);
};
promiseExecution();
"result:", [{
  status: "fulfilled",
  value: true
}, {
  status: "fulfilled",
  value: true
}, {
  reason: false,
  status: "rejected"
}, {
  reason: false,
  status: "rejected"
}]

Any

‘any’ is the ugly child of the bunch. There’s very few scenarios where you’re likely to use it. The reason being is that the ‘any’ method will execute a series of promises, much alike all other methods mentioned here. However, it is fulfilled upon the first promise that resolves. Meaning we would need to be in a situation where we have multiple promises to execute, but only care about one resolving, and not just one resolving, but the first one that resolves.

const promiseResolvesFast = new Promise((resolve, reject) =>
  setTimeout(() => resolve('I am a very speedy boi'), 100),
);
promiseResolvesSlow = new Promise((resolve, reject) =>
  setTimeout(() => resolve('I am a little slow boi'), 500),
);

const promiseRejects = new Promise((resolve, reject) => reject(false));

const promiseExecution = async () => {
  const promiseAll = await Promise.any([promiseResolvesFast, promiseResolvesSlow, promiseRejects]);

  console.log('result:', promiseAll);
};
promiseExecution();
result: I am a very speedy boi

Even if we have a promise that rejects, we disregard that, and continue with execution until one has resolved. If none have resolved, and they’ve all rejected we instead have to handle that with a catch.

const promiseRejects = new Promise((resolve, reject) => reject(false));

const promiseExecution = async () => {
  const promiseAll = await Promise.any([promiseRejects, promiseRejects]).catch((e) =>
    console.log('error:', e),
  );

  console.log('result:', promiseAll);
};
promiseExecution();
"error:", [object Error] { ... }
"result:", undefined

Race

Sometimes everything is about speed, we want to take action as soon as the first promise either becomes fulfilled, or rejected. This can be handy in scenarios where we could end up with an infinite load, or we’re only allowing the user to wait for so long.

The classic example for using race practically is having a failure scenario for when a request has taken too long.

Imagine a user is sitting on a page and is waiting for the result of a query. It’s been five seconds, still waiting… Ten seconds, still waiting… Now it’s been fifteen whole seconds! That’s too long. So let’s allow the failure scenario to take place.

const promiseInfiniteLoad = new Promise((resolve, reject) =>
  setTimeout(() => resolve('Lets load infinitely'), 30000),
);

const promiseTimeout = new Promise((resolve, reject) =>
  setTimeout(() => resolve('Looks like loading is taking too long'), 1500),
);

const promiseExecution = async () => {
  const promiseAll = await Promise.race([promiseInfiniteLoad, promiseTimeout]);

  console.log('result:', promiseAll);
};
promiseExecution();
result: Looks like loading is taking too long

When we add in a promise that rejects immediately, since it has finished execution first we take the result of that. Although since it’s a rejection, inline with previous rejection cases, it has to be caught and dealt with.

const promiseInfiniteLoad = new Promise((resolve, reject) =>
  setTimeout(() => resolve('Lets load infinitely'), 30000),
);

const promiseTimeout = new Promise((resolve, reject) =>
  setTimeout(() => resolve('Looks like loading is taking too long'), 1500),
);

const promiseReject = new Promise((resolve, reject) => reject('lets aim to fail'));

const promiseExecution = async () => {
  const promiseAll = await Promise.race([promiseInfiniteLoad, promiseTimeout, promiseReject]).catch(
    (e) => console.log('error:', e),
  );

  console.log('result:', promiseAll);
};
promiseExecution();
error: lets aim to fail
result: undefined

Conclusion

So we’ve learnt about the four available static methods for the Promise API. How they operate, how to use them, and how to handle failure cases.

Have you used all four before?

What use have you found for Promise.Any?