DEV Community

Cover image for What's wrong with Promise.allSettled() and Promise.any()❓
Vitaliy Potapov
Vitaliy Potapov

Posted on • Updated on

What's wrong with Promise.allSettled() and Promise.any()❓

I've recently read the Promise combinators article in v8 blog. It's about two upcoming methods in Promise API: Promise.allSettled() and Promise.any(). And I feel frustrated. The design of these methods looks to me inconsistent with current Promise API. Let me share my opinion below.

Promise.allSettled

According to article: 

Promise.allSettled gives you a signal when all the input promises are settled, which means they're either fulfilled or rejected.

The use-case is to send several API calls and wait for all finished:

const promises = [
  fetch('/api-call-1'),
  fetch('/api-call-2'),
  fetch('/api-call-3'),
];

await Promise.allSettled(promises);

removeLoadingIndicator();
Enter fullscreen mode Exit fullscreen mode

For sure, this is useful. But this task can be easily solved with .map() and Promise.all(). The change is minimal:

const promises = [
  fetch('/api-call-1'),
  fetch('/api-call-2'),
  fetch('/api-call-3'),
].map(p => p.catch(e => e)); // <-- the only change

await Promise.all(promises);

removeLoadingIndicator();
Enter fullscreen mode Exit fullscreen mode

Is it worth implementing a new core method that can be solved in a few lines of code? As for me this is a library-land feature, not the core API method.

But more important is that Promise.allSettled brings extra abstraction and increases code complexity. Unlike Promise.all it fulfills with array of wrapping objects {status, reason} instead of pure promise values. As a developer I don't like it. I expect that methods with similar names .all()/.allSettled() behave similar ways. But they don't.

Moreover, the code with Promise.allSettled encourages worse errors control. Errors should be filtered out from the final result instead of traditionally handled in catch blocks. This, in turn, has the following downsides:

  • errors are not handled immediately, at the moment when they occur. In case of several related errors you can't know which was the original one. And log will contain incorrect timestamps.
  • errors are not handled if at least one promise is pending forever.

The approach with current Promise.all does not allow such things.

Promise.any

Promise.any gives you a signal as soon as one of the promises fulfills.

In other words Promise.any is Promise.race that ignores rejections.

The use-case is to check several endpoints and take data from the first successful one:

const promises = [
  fetch('/endpoint-a').then(() => 'a'),
  fetch('/endpoint-b').then(() => 'b'),
  fetch('/endpoint-c').then(() => 'c'),
];
try {
  const first = await Promise.any(promises);
} catch (error) {
  // All of the promises were rejected.
  console.log(error);
}
Enter fullscreen mode Exit fullscreen mode

I agree sometimes it may be useful. But how often? In how many projects did you use the pattern make several parallel requests to identical endpoints for the same data? Feel free to share in comments. But from my vision - not very often. Couldn't it be more useful for community to get native implementation of bluebird's Promise.each() or Promise.delay()?

Moreover, Promise.any introduces a new type of error - AggregateError. Such error contains links to other errors if all promises are rejected. One more error handling approach! It differs from Promise.allSettled where errors are extracted from success result. It also differs from Promise.all/Promise.race which reject with just an Error instance. How will JavaScript look like if every new Promise API method will introduce new way of error handling? Although the proposal is on a very early stage, I'm concerned about the direction.

Based on current Promise API the implementation of Promise.any is a bit tricky but actually two lines of code:

const reverse = p => new Promise((resolve, reject) => Promise.resolve(p).then(reject, resolve));
Promise.any = arr => reverse(Promise.all(arr.map(reverse)));
Enter fullscreen mode Exit fullscreen mode

Shouldn't we leave it in library-land and keep core Promise API clean and simple?

Inconsistency

Why Promise.all and Promise.race are so pretty?

Because they behave very consistent and similar to usual promises: fulfill with just a value and reject with just an error. No wrapped values, no accumulated errors, no extra complexity.

Why Promise.allSettled and Promise.any are so weird to me?

  • Promise.allSettled fulfills with array of objects with status and reason wrapping underlying promise values. And rejects… never.
  • Promise.any fulfills with single value and ignores intermediate rejections. Only if all promises are rejected it rejects with accumulated reason wrapping all underlying reasons.

These new approaches are really difficult to put in my head. As they are quite different from the current Promise API.

I expect a popular job interview question in 2020:
What's the difference of these four methods?

  1. Promise.all()
  2. Promise.allSettled()
  3. Promise.race()
  4. Promise.any()

Although it's cool question I don't think core API should encourage such complexity.

Naming

I'm also disappointed with naming. Four methods with slightly different behavior should have pretty clear names. Otherwise I have to re-check MDN every time I meet them in code. From the proposal of Promise.any:

It clearly describes what it does

Let me to disagree. For me the name of Promise.any is confusing:

  • will it fulfill if any of promises fulfills? Yes.
  • will it reject if any of promises rejects? No.
  • will it settle if any of promises settle? It depends.
  • how it differs from Promise.race? Hmm..

I think, the name of each method should explicitly define the condition when the method fulfills. I would suggest the following naming convention:

Promise.all        -> Promise.allFulfilled
Promise.allSettled -> Promise.allSettled
Promise.race       -> Promise.oneSettled
Promise.any        -> Promise.oneFulfilled
Enter fullscreen mode Exit fullscreen mode

It reflects four possible combinations of promise states. It explains why these methods are referenced as combinators in proposal.
Of course, such rename is not possible as Promise.all and Promise.race already landed and used in many applications. But for new methods having some naming strategy would be very helpful.

I've opened issue in Promise.any() proposal repository on GitHub, you are welcome to share you thoughts.

Swallowed rejections

In general I'm not inspired with the concept of non-thrown "swallowed" rejections introduced in new methods. In fact, new Promise API provides a way to silently ignore errors in the code:

  • Promise.allSettled never rejects.
  • Promise.any rejects only if all promises rejected.

Currently no other core JavaScript API does that. The only way to ignore an error - manually wrap it into try..catch / .catch() with empty body. And write a comment why do you ignore error here, otherwise eslint will warn you.

I think the core API should expose all errors. It is always a developer decision whether to handle error or not. It should be explicit for other developers. Just imagine how many hours of debugging will be spent due to inaccurate usage of swallowed rejections! Especially when dealing with third-party code— when something does not work and no errors thrown.

Conclusion

I use promises every working day. As well as many other developers do. I love JavaScript for its async nature. Having clear and intuitive API allows me to solve tasks faster and be more productive. That's why I think Promise API should be treated and changed very carefully.
Thanks for reading and welcome to comments.

This post appeared first on hackernoon.com.

Top comments (10)

Collapse
 
ressom profile image
Jason Mosser

allSettled is useful in at least two principle cases. First, when calling multiple operations that are recoverable on failure. Second, calling multiple operations where 'silent' behavior is desired.

It avoids using errors for flow control, which is good.

const [r1, r2] = await Promise.allSettled([op1(), op2()]);

// decode state of the operations and retry if needed //
// or, if one does not care about the results, just invoke it and move on //
Collapse
 
vitalets profile image
Vitaliy Potapov • Edited

I totally agree - this is useful scenario.
My point is that it can be easily achieved with current api without introducing new methods:

const [r1, r2] = await Promise.all([op1(), op2()].map(p => p.catch(e => e)));

// decode state of operations by simply checking "instanceof Error".
Collapse
 
itscodingthing profile image
Bhanu Pratap Singh

I think it is more about the complexity or how much code you write to achieve same functionality without worrying about the other things.

Collapse
 
zakalwe314 profile image
Euan Smith

I use Promise.any A LOT. I'm using it again on a project right now. I was overjoyed to see it proposed to become part of the spec. Frankly, it is what Promice.race should have been in the first place. So, let me ask you a question back - while you might not need Promise.any for you code, have you every used Promise.race?

I would also disagree with your assertion that it is different in nature from other in-build Promise calls. It is actually the dual of Promise.all. Promise.any will return with the FIRST SUCCESS or ALL FAILURES. Promise.all will return the FIRST FAILURE or ALL SUCCESSES (which points to a quick and dirty way of implementing it, although I wouldn't really recommend it).

It is true that the aggregate error feels a bit clunky to say the least, however I can't see an alternative and I'm certainly willing to put up with it.

Collapse
 
kimamula profile image
Kenji Imamula • Edited

I use Promise.race for implementing timeout. See, for example, here to see how Promise.race can be used to implement timeout.

Currently I am looking for good real world examples of the use case of Promise.any and I would appreciate if you could share your use case.

Collapse
 
zakalwe314 profile image
Euan Smith • Edited

My typical use case is that I have a number of possible locations of a given resource. There may be multiple servers for the same thing (e.g. primary and backup, or different geographical locations). Some of these might fail, but you don't care so long as you can find one which succeeds. In this case Promise.any is the right pattern as it only rejects if ALL of the endpoints reject.

A second use case is that I have multiple routes to the same server. One might be a route via the internet and one might be locally over a LAN - both or neither of these routes might be available. Again Promise.any is the right pattern.

If what you do is work with cloud services then it is possible you will never need this - you don't have availability or routing complications. However I work with distributed services and IoT devices (we develop the hardware and the software) sometimes operating in networks with poor or otherwise limited connectivity. In this case Promise.any is an essential tool in establishing a connection.

Thread Thread
 
kimamula profile image
Kenji Imamula

Thanks for sharing! That makes sense.

Collapse
 
yegorzaremba profile image
Yegor <3

we will use Promise.any here github.com/nsfw-filter/nsfw-filter...

Collapse
 
larsivi profile image
Lars Ivar Igesund

Late to the party I guess, but there are also some other gotchas in Promise.allSettled.

You show the example with 3 fetch calls in an array, which you so pass to allSettled. Depending on what else is going on, maybe the logic to build the promise array is a bit convoluted and maybe a bit slow. If you want to handle any errors coming out of those promises, you will in fact have missed them if the promises settled prior to getting into allSettled.

I also find it rather annoying that you can't return a value with the rejected promises. If you're going to handle the rejections, you would really like more information, in a structured manner, rather than just get access to an error string/reason.

Collapse
 
tblabs profile image
Tomasz Budrewicz

+1