Search code examples
javascriptpromisees6-promise

Promise.all([]) returns a resolved promise, but Promise.race([]) returns a pending promise. Why are they different?


If either Promise.all or Promise.race are called with a non-empty array, they return a pending promise:

console.log(Promise.all([1]));
// prints Promise {<pending>}
console.log(Promise.race([1]));
// returns Promise {<pending>}

If Promise.race is called with an empty array, it returns a pending promise:

console.log(Promise.race([]));
// prints Promise {<pending>}

But if Promise.all is called with an empty array, it returns a promise that is already resolved:

console.log(Promise.all([]));
// prints Promise {<resolved>: Array(0)}

Why was the Promise.all function designed like this? It seems like there's no good reason for the inconsistency, but maybe I'm missing something.


Solution

  • From the EMCA Script specification for Promise.race():

    If the iterable argument is empty or if none of the promises in iterable ever settle then the pending promise returned by this method will never be settled


    The Promise.all() specification is not quite so easy to follow in this regard, but basically when you pass it an empty array, it starts out with what they refer to as a remainingElementsCount of 0 which lets it resolve immediately.

    When you pass it a value as in Promise.all([1]), it is likely wrapping that value in Promise.resolve() and then tracking the .then() handler from that which will resolve on the next tick so console.log(Promise.all([1])) shows it still pending before the next tick.


    Logically, there's some sense to this. Promise.race() is supposed to resolve to the value of the first promise to resolve, but if you don't pass it anything, then there really is no first resolved value. The only other options would be to reject or resolve to undefined or throw an exception for invalid usage. I'm not quite sure why the designers chose the outcome they did vs. these other choices, but at least it is clearly detailed in the spec.

    Promise.all(), on the other hand, can resolve to an empty array just fine and that's a logical outcome to passing an empty array.

    Why was the Promise.all function designed like this?

    To really "know" the designers logic, you'd have to ask one of them or have been in the discussion or find mailing list discussions where the logic was discussed.

    But, one can make an argument that if you had a variable length array of things you wanted to wait for to complete with Promise.all() that the function should work whether the array had 20 items in it or 0. In the case for a 0 length array, it would just resolve immediately on the next tick and that would be both useful and consistent as there are no promises to wait for and there's a resolved value (an empty array) that fits and is consistent.


    ES6 Discussion on the Topic

    Here's a link to a development discussion of Promise.race() never resolving: https://github.com/domenic/promises-unwrapping/issues/75. There were certainly people who disagreed with the current implementation.

    My personal opinion (shared by some others in various discussions of this topic) is that it should have thrown an exception because it's basically an invalid condition and from a development point of view "fail fast and noticeable" is much better than an infinite promise. But, there were apparently more people who liked it the way it is.

    The Bluebird docs recommend using their Promise.any() instead of Promise.race(), partially because it does not have this behavior.