Why does JavaScript's `Promise.all` not run al

2019-02-14 16:20发布

问题:

According to MDN:

If any of the passed in promises rejects, the all Promise immediately rejects with the value of the promise that rejected, discarding all the other promises whether or not they have resolved.

The ES6 spec seems to confirm this.

My question is: Why does Promise.all discard promises if any of them reject, since I would expect it to wait for "all" promises to settle, and what exactly does "discard" mean? (It's hard to tell what "discard" means for in-flight promises vs. promises that may not have run yet.)

I ask because I frequently run into situations where I have a list of promises and want to wait for them all to settle and get all rejections that may have occurred, which Promise.all doesn't cater to. Instead, I have to use a hack like this:

const promises = []; // Array of promises
const settle = promise => promise.then(result => ({ result }), reason => ({ reason }));
Promise.all(promises.map(settle))
  .then(/ * check "reason" property in each element for rejection */);

回答1:

The asynchronous operations associated with the promises are all run. If one of those promises rejects, then Promise.all() simply does not wait for all of them to complete, it rejects when the first promise rejects. That is just how it was designed to work. If you need different logic (like you want to wait for all of them to be done, no matter whether they fulfill or reject), then you can't use just Promise.all().

Remember, a promise is not the async operation itself. A promise is just an object that keeps track of the state of the async operation. So, when you pass an array of promises to Promise.all(), all those async operations have already been started and are all in-flight already. They won't be stopped or cancelled.

Why does Promise.all discard promises if any of them reject, since I would expect it to wait for "all" promises to settle.

It works the way it does because that's how it was designed and that is a very common use case when you don't want your code to continue if there was any sort of error. If it happens to not be your use case, then you need to use some implementation of .settle() which has the behavior you want (which you seem to already know).

What I find the more interesting question is why is there not a .settle() option in the specification and standard implementation since it is also a fairly common use case. Fortunately, as you have found, it is not a lot of code to make your own. When I don't need the actual reject reason and just want some indicator value to be placed into the array, I often use this fairly simple to use version:

// settle all promises.  For rejeted promises, return a specific rejectVal that is
// distinguishable from your successful return values (often null or 0 or "" or {})
Promise.settleVal = function(rejectVal, promises) {
    return Promise.all(promises.map(function(p) {
        // make sure any values or foreign promises are wrapped in a promise
        return Promise.resolve(p).catch(function(err) {
            // instead of rejection, just return the rejectVal (often null or 0 or "" or {})
            return rejectVal;
        });
    }));
};

// sample usage:
Promise.settleVal(null, someArrayOfPromises).then(function(results) {
    results.forEach(function(r) {
        // log successful ones
        if (r !== null) {
           console.log(r);
        }
    });
});

what exactly does "discard" mean?

It just means that the promises are no longer tracked by Promise.all(). The async operations they are associated with keep right on doing whatever they were going to do. And, in fact if those promises have .then() handlers on them, they will be called just as they normally would. discard does seem like an unfortunate term to use here. Nothing happens other than Promise.all() stops paying attention to them.


FYI, if I want a more robust version of .settle() that keeps track of all results and reject reasons, then I use this:

// ES6 version of settle that returns an instanceof Error for promises that rejected
Promise.settle = function(promises) {
    return Promise.all(promises.map(function(p) {
        // make sure any values or foreign promises are wrapped in a promise
        return Promise.resolve(p).catch(function(err) {
            // make sure error is wrapped in Error object so we can reliably detect which promises rejected
            if (err instanceof Error) {
                return err;
            } else {
                var errObject = new Error();
                errObject.rejectErr = err;
                return errObject;
            }
        });
    }));
}

// usage
Promise.settle(someArrayOfPromises).then(function(results) {
    results.forEach(function(r) {
       if (r instanceof Error) {
           console.log("reject reason", r.rejectErr);
       } else {
           // fulfilled value
           console.log("fulfilled value:", r);
       }
    });
});

This resolves to an array of results. If a result is instanceof Error, then it was a rejected, otherwise it's a fulfilled value.



回答2:

Because Promise.all guarantees they all succeeded. Simple as that.

It's the most useful building block along with Promise.race. Everything else can be built on those.

There's no settle, because it's so trivial to build like this:

Promise.all([a(), b(), c()].map(p => p.catch(e => e)))

There's no easy way to build Promise.all on top of settle, which may be why it's not the default. settle would also have had to standardize a way to distinguish success values from errors, which may be subjective and depend on the situation.



回答3:

I'd argue, because rejecting a promise is like throwing an error in sync code, and an uncatched error in sync code also interrupts the execution.

Or one could argue for the assumption that requesting all these promises and composing them into a combined Array, and then waiting for all these to finish implies that you need them all to proceed with whatever you've intended to do, and if one of them fails your intended task lacks one of its dependencies, and it is logical to simply forward the reason for the fail till this reason is somehow handled/caught.