DEV Community

Samuel Rouse
Samuel Rouse

Posted on

Do we need Promise.allSettled()?

What do you think about Promise.allSettled()?

To me, allSettled looks like a solution in search of a problem. That problem is developers not handling errors.

The Concept

Promise.allSettled() has a very simple design:

const allSettled = (promises) => Promise.all(promises.map(entry => entry
  .then((value) => ({ status: 'fulfilled', value }))
  .catch((reason) => ({ status: 'rejected', reason }))
));
Enter fullscreen mode Exit fullscreen mode

It provides a "consistent" outcome object – well, status is consistent so you can .filter() more cleanly than using Object.hasOwn(), but value and reason are intentionally different so you can't mix them up.

Mostly, allSettled adds a .catch() to each promise for you.

Handle Your Errors

But here's my sticking point: if you are calling a group of services in parallel and you know one or more can fail, yet it doesn't really matter...why aren't you writing error handling for that?

const getFlakyService = (payload) => fetch(flakyUrl, payload);

Promise.allSettled([
  getFlakyService({ type: 'reptiles' }),
  getFlakyService({ type: 'mammals' }),
  getFlakyService({ type: 'birds' }),
  getFlakyService({ type: 'amphibians' }),
]).then((outcomes) => outcomes
  .filter(({ status }) => status === 'fulfilled'))
});
Enter fullscreen mode Exit fullscreen mode

How much effort are we saving compared to this:

const getFlakyService = (payload) => fetch(flakyUrl, payload)
  // We don't really care about the failures
  .catch(() => undefined);

Promise.all([
  getFlakyService({ type: 'reptiles' }),
  getFlakyService({ type: 'mammals' }),
  getFlakyService({ type: 'birds' }),
  getFlakyService({ type: 'amphibians' }),
]).then((data) => { /* ... */ });
Enter fullscreen mode Exit fullscreen mode

If you care about which calls are failing, you likely need the request information accessible for tracking, which isn't guaranteed to be available in the reason. Promise.allSettled is even less helpful in this case and it makes more sense to write your own error handling.

const getFlakyService = (payload) => fetch(flakyUrl, payload)
  // Send the failures details to a tracking/logging layer
  .catch((error) => trackRequestError(flakyUrl, payload, error);

Promise.all([
  getFlakyService({ type: 'reptiles' }),
  getFlakyService({ type: 'mammals' }),
  getFlakyService({ type: 'birds' }),
  getFlakyService({ type: 'amphibians' }),
]).then((data) => { /* ... */ });
Enter fullscreen mode Exit fullscreen mode

I will grant that the standardization of the "outcome" could be convenient. With allSettled you can count the failures once they all complete. But that's true with custom error handling as well.

Conclusion

I'll continue to use Promise.all() for the near future, but I'm interested to hear about your use cases for Promise.allSettled() and why you prefer it.

Top comments (3)

Collapse
 
miketalbot profile image
Mike Talbot ⭐ • Edited

In my codebase, I use allSettled in three places. In each place, it is used to raise a notification to the front end when a series of rate-limited jobs have been completed. Each job handles its error states, so the allSettled is used by a separate mechanism to notify the user of the overall state of the process.

I guess specifically, the jobs are packaged and started because of their "container", which needs to be reported to the user, but they are atomic. The code is cleaner, not handling error messages as error messages are irrelevant to the overarching state of the processing of the work.

So I use it, but very very rarely... I use it about the same amount as Promise.race - which I am only using for timeouts.

Collapse
 
oculus42 profile image
Samuel Rouse

Thanks for the reply! With these different uses, are you "splitting" the promise, where one chain from the original request handles the job and another the status?

That's a really interesting use case that provides some new perspective!

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Yes, exactly that.