62

What is the difference between Promise.any() and Promise.race(), and how do they get used differently?


From MDN,

Also, unlike Promise.race(), which returns the first settled value, this method returns the first resolved value. This method will ignore all rejected promises up until the first promise that resolves.

So that brings me to, the difference between resolved and settled. Which then brought me to the MDN promises page, which then brought me to States and Fates

Being settled is not a state, just a linguistic convenience.

So we have Promise.any and Promise.race for linguistic convenience? i.e. there is no difference. Another instance of this equality, is "A promise whose fate is unresolved is necessarily pending." and "We say that a promise is settled if it is not pending, i.e. if it is either fulfilled or rejected.".

So if a promise is resolved, it is not unresolved, so it is not pending. So then, if it's not pending, it's settled. So resolved === settled.

BinaryButterfly
  • 18,137
  • 13
  • 50
  • 91
Ben Butterworth
  • 22,056
  • 10
  • 114
  • 167
  • 1
    Settled means done, resolved means done **successfully** – apokryfos May 11 '20 at 14:26
  • 1
    settled = resolved or rejected. resolved = not rejected – VLAZ May 11 '20 at 14:26
  • "*So resolved === settled*" it's not full equality. `dog = animal` is true but `anima = dog` is false. Settled is a super category. – VLAZ May 11 '20 at 14:28
  • Okay I think i understand it now, should I delete this question – Ben Butterworth May 11 '20 at 14:28
  • Oh wait, no. According to 'States and Fates', "A promise whose fate is resolved can be in any of the three states: fulfilled, rejected or pending". – Ben Butterworth May 11 '20 at 14:30
  • i think comments above are confusing resolved with fulfilled. – Ben Butterworth May 11 '20 at 14:31
  • 2
    @apokryfos - No, "resolved" means that the promise either *is* settled or *will be* settled by the result of another promise. A resolved promise can still be pending. The *states* are pending, fulfilled, or rejected. – T.J. Crowder May 11 '20 at 14:31
  • @T.J.Crowder there's no practical way to get a resolved promise that's still pending though since the resolved value is chained – apokryfos May 11 '20 at 14:33
  • @apokryfos - I don't know what you mean by that. There's definitely a substantial difference between a *resolved* promise and a *fulfilled* one, and you can easily observe that a promise is pending (hook fulfillment and rejection and wait a tick -- if you didn't get a callback, the promise was pending during that time). – T.J. Crowder May 11 '20 at 14:44
  • 2
    @BenB I've fixed the MDN page with [proper terminology](https://stackoverflow.com/a/29269515/1048572) – Bergi May 11 '20 at 14:54
  • 2
    @apokryfos `Promise.resolve({then(){}})` is the shortest example of a resolved but pending promise. In general, you can resolve a promise with another promise, which means you cannot resolve it again, but when that other promise is still pending then so is yours. – Bergi May 11 '20 at 14:55
  • @Bergi my point is `Promise.any` or `Promise.race` will not return until the entire chain is settled or resolved so there's no practical way to get a result from either of these functions that's still pending, as far as I know – apokryfos May 11 '20 at 15:06
  • @apokryfos of course the promise that `Promise.any` does return is pending until the condition for settling it is met. (And before further confusion occurs, my comment to you did respond to "*there's no practical way to get a resolved promise that's still pending*") – Bergi May 11 '20 at 15:17

1 Answers1

116

Promise.race and Promise.any do different things:

Promise.race is settled as soon as any of the promises you feed it settle, whether they are fulfilled or rejected.

Promise.any is settled as soon as any of the promises you feed it is fulfilled or they are all rejected, in which case it's rejected with an AggregateError.

The chief differences are:

  1. race's promise is rejected when the first promise you give it is rejected; any's promise isn't, because another promise may be fulfilled instead.

  2. any's promise's rejection reason will be an AggregateError, but race's rejection reason will be the rejection reason from the first promise that was rejected.

So if you pass them both an array of two promises, and one of the promises is rejected, then afterward the other promise is fulfilled, the promise from Promise.race will be rejected (because the first promise to settle was rejected) and the promise from Promise.any will be fulfilled (because although the first promise was rejected, the second was fulfilled). E.g.:

const a = new Promise((_, reject) => setTimeout(reject,  100, new Error("a")));
const b = new Promise((resolve)   => setTimeout(resolve, 200, "b"));

Promise.race([a, b]).then(
    value => {
        console.log(`race: fulfilled with ${value}`);
    },
    reason => {
        console.log(`race: rejected with ${reason.message}`);
    }
);

Promise.any([a, b]).then(
    value => {
        console.log(`any:  fulfilled with ${value}`);
    },
    reason => {
        console.log(`any:  rejected with ${reason.errors.map(({message}) => message).join()}`);
    }
);

With a JavaScript engine that has Promise.any (or a polyfill), that outputs

race: rejected with a
any:  fulfilled with b

Play with various outcomes here (there's a very rough incomplete stand-in for Promise.any included if your browser doesn't have it yet):

addFakeAnyIfMissing();

document.querySelector("input[value='Start Again']").addEventListener("click", run);

run();

function setupPromise(name) {
    return new Promise((resolve, reject) => {
        const div = document.querySelector(`[data-for="${name}"]`);
        const btnFulfill = div.querySelector("input[value=Fulfill]");
        const btnReject  = div.querySelector("input[value=Reject]");;
        const display    = div.querySelector(".display");
        btnFulfill.disabled = btnReject.disabled = false;
        display.textContent = "pending";
        btnFulfill.onclick = () => {
            resolve(name);
            display.textContent = `fulfilled with ${name}`;
            btnFulfill.disabled = btnReject.disabled = true;
        };
        btnReject.onclick = () => {
            reject(new Error(name));
            display.textContent = `rejected with Error(${name})`;
            btnFulfill.disabled = btnReject.disabled = true;
        };
    });
}

function run() {
    const a = setupPromise("a");
    const b = setupPromise("b");
    const raceDisplay = document.querySelector("[data-for=race] .display");
    const anyDisplay  = document.querySelector("[data-for=any]  .display");
    raceDisplay.textContent = anyDisplay.textContent = "pending";

    Promise.race([a, b]).then(
        value => {
            raceDisplay.textContent = `fulfilled with ${value}`;
        },
        reason => {
            raceDisplay.textContent = `rejected with ${reason.message}`;
        }
    );

    Promise.any([a, b]).then(
        value => {
            anyDisplay.textContent = `fulfilled with ${value}`;
        },
        reason => {
            anyDisplay.textContent = `rejected with ${reason.errors.map(({message}) => message).join()}`;
        }
    );
}

function addFakeAnyIfMissing() {
    if (!Promise.any) {
        // VERY ROUGH STANDIN, not a valid polyfill
        class AggregateError extends Error {}
        Object.defineProperty(Promise, "any", {
            value(iterable) {
                return new Promise((resolve, reject) => {
                    const errors = [];
                    let waitingFor = 0;
                    for (const value of iterable) {
                        const index = waitingFor++;
                        Promise.resolve(value).then(
                            value => {
                                resolve(value);
                                --waitingFor;
                            },
                            reason => {
                                errors[index] = reason;
                                if (--waitingFor === 0) {
                                    reject(Object.assign(new AggregateError(), {errors}));
                                }
                            }
                        );
                    }
                });
            },
            writable: true,
            configurable: true
        });
    }
}
<div data-for="a">
    Promise A
    <input type="button" value="Fulfill">
    <input type="button" value="Reject">
    <span class="display"></span>
</div>
<div data-for="b">
    Promise B
    <input type="button" value="Fulfill">
    <input type="button" value="Reject">
    <span class="display"></span>
</div>
<div data-for="race">
    <code>Promise.race([a, b])</code>:
    <span class="display"></span>
</div>
<div data-for="any">
    <code>Promise.any([a, b])</code>:
    <span class="display"></span>
</div>
<input type="button" value="Start Again">

This chart from the proposal may help:

There are four main combinators in the Promise landscape.

+−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−+
| name               | description                                     |                 |
+−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−+
| Promise.allSettled | does not short-circuit                          | Added in ES2020 |
| Promise.all        | short-circuits when an input value is rejected  | Added in ES2015 |
| Promise.race       | short-circuits when an input value is settled   | Added in ES2015 |
| Promise.any        | short-circuits when an input value is fulfilled | this proposal   |
+−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−+

Continuing with your question...

Another instance of this equality, is "A promise whose fate is unresolved is necessarily pending." and "We say that a promise is settled if it is not pending, i.e. if it is either fulfilled or rejected.".

So if a promise is resolved, it is not unresolved, so it is not pending. So then, if its not pending, its settled. So resolved === settled.

I can see how you got there, but you can't invert it like that. :-) A resolved promise can be pending. It's just that an unresolved promise is definitely pending.

The states are:

  • pending
  • fulfilled
  • rejected

You can resolve a promise (A) to another promise (B), which means that while A may still be pending, nothing can change what's going to happen to it; its fate is sealed, it will be fulfilled or rejected according to what happens to B.

(More about this in my blog post Let's talk about how to talk about promises.)

Here's an example of a pending resolved promise:

const b = new Promise((resolve, reject) => {
    setTimeout(() => {
        if (Math.random() < 0.5) {
            resolve("all good");
        } else {
            reject(new Error("ugh"));
        }
    }, 100);
});

// (Being verbose for clarity)
const a = new Promise((resolve, reject) => {
    resolve(b);
    // Now, `a` is pending, but resolved
    // No matter what else we do, `a`'s fate is tied to
    // `b`'s. For instance, this does nothing:
    resolve("foo");
    // Neither does this:
    reject(new Error("foo"));
});

b
.then(value => {
    console.log(`b was fulfilled: ${value}`);
})
.catch(reason => {
    console.log(`b was rejected: ${reason.message}`);
});

a
.then(value => {
    console.log(`a was fulfilled: ${value}`);
})
.catch(reason => {
    console.log(`a was rejected: ${reason.message}`);
});
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 6
    Thanks for all of the code examples. I suspect this answer will prove useful to coders for years to come! – rinogo Dec 09 '20 at 23:44
  • hi @T.J. Crowder excellent+1, there `b` at the beginning is unresolved because it doesn't know if `resolve()` or `reject()` is going to be called and because it is not associated to another promise either, right? –  Mar 11 '23 at 20:34
  • Speaking of the last code example in your answer @T.J. Crowder –  Mar 11 '23 at 20:35
  • 1
    @GeorgeMeijer - Nearly: It's unresolved because its fate isn't tied to another promise. Even if we knew it was going to be fulfilled (for instance, the standard [sleep promise](https://stackoverflow.com/a/39914235/157247)), I don't think we'd say it was *resolved*, per se. But I see your point that it's similar. – T.J. Crowder Mar 12 '23 at 08:49