10

Let's say I have a Promise.all() that handles two promises. If one promise produces an error, but the other resolves, I would like to be able to handle the errors based on the situation after the Promise.all() has settled.

ES6 Promises are missing the settle method, I'm assuming for a good reason. But I can't help but think that the .settle() method would make this problem a lot easier for me.

Am I going about this the wrong way or is extending the ES6 Promises with a settle method the right thing to do here?

An example of how I am thinking of using .settle():

Promise.all([Action1,Action2])
.settle(function(arrayOfSettledValues) 
    //if 1 failed but not 2, handle
    //if 2 failed but not 1, handle
    //etc....
)
Elliot
  • 1,893
  • 4
  • 17
  • 35

1 Answers1

20

Am I going about this the wrong way or is extending the ES6 Promises with a settle method the right thing to do here?

You can't directly use Promise.all() to generate .settle() type behavior that gets you all the results whether any reject or not because Promise.all() is "fast-fail" and returns as soon as the first promise rejects and it only returns that reject reason, none of the other results.

So, something different is needed. Often times, the simplest way to solve that problem is by just adding a .then() handler to whatever operation creates your array of promises such that it catches any rejects and turns them into fulfilled promises with some specific value that you can test for. But, that type of solution is implementation dependent as it depends upon exactly what type of value you are returning so that isn't entirely generic.

If you want a generic solution, then something like .settle() is quite useful.

You can't use the structure:

Promise.all([...]).settle(...).then(...);

Note (added in 2019): It appears the Promise standards effort has picked Promise.allSettled() as a standard implementation of "settle-like" behavior. You can see more on that at the end of this answer.

Because Promise.all() rejects when the first promise you pass it rejects and it returns only that rejection. The .settle() logic works like:

Promise.settle([...]).then(...);

And, if you're interested, here's a fairly simple implementation of Promise.settle():

// ES6 version of settle
Promise.settle = function(promises) {
    function PromiseInspection(fulfilled, val) {
        return {
            isFulfilled: function() {
                return fulfilled;
            }, isRejected: function() {
                return !fulfilled;
            }, isPending: function() {
                // PromiseInspection objects created here are never pending
                return false;
            }, value: function() {
                if (!fulfilled) {
                    throw new Error("Can't call .value() on a promise that is not fulfilled");
                }
                return val;
            }, reason: function() {
                if (fulfilled) {
                    throw new Error("Can't call .reason() on a promise that is fulfilled");
                }
                return val;
            }
        };
    }

    return Promise.all(promises.map(function(p) {
        // make sure any values are wrapped in a promise
        return Promise.resolve(p).then(function(val) {
            return new PromiseInspection(true, val);
        }, function(err) {
            return new PromiseInspection(false, err);
        });
    }));
}

In this implementation, Promise.settle() will always resolve (never reject) and it resolves with an array of PromiseInspection objects which allows you to test each individual result to see whether it resolved or rejected and what was the value or reason for each. It works by attaching a .then() handler to each promise passed in that handles either the resolve or reject from that promise and puts the result into a PromiseInspection object which then becomes the resolved value of the promise.

You would then use this implementation like this;

Promise.settle([...]).then(function(results) {
    results.forEach(function(pi, index) {
        if (pi.isFulfilled()) {
            console.log("p[" + index + "] is fulfilled with value = ", pi.value());
        } else {
            console.log("p[" + index + "] is rejected with reasons = ", pi.reason());
        }
    });
});

FYI, I've written another version of .settle myself that I call .settleVal() and I often find it easier to use when you don't need the actual reject reason, you just want to know if a given array slot was rejected or not. In this version, you pass in a default value that should be substituted for any rejected promise. Then, you just get a flat array of values returned and any that are set to the default value where rejected. For example, you can often pick a rejectVal of null or 0 or "" or {} and it makes the results easier to deal with. Here's the function:

// settle all promises.  For rejected 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).then(null, function(err) {
            // instead of rejection, just return the rejectVal (often null or 0 or "" or {})
            return rejectVal;
        });
    }));
};

And, then you use it like this:

Promise.settleVal(null, [...]).then(function(results) {
    results.forEach(function(pi, index) {
        if (pi !== null) {
            console.log("p[" + index + "] is fulfilled with value = ", pi);
        }
    });
});

This isn't an entire replacement for .settle() because sometimes you may want to know the actual reason it was rejected or you can't easily distinguish a rejected value from a non-rejected value. But, I find that more than 90% of the time, this is simpler to use.


Here's my latest simplification for .settle() that leaves an instanceof Error in the return array as the means of distinguishing between resolved values and rejected errors:

// settle all promises.  For rejected promises, leave an Error object in the returned array
Promise.settleVal = 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) {
            let returnVal = err;
            // instead of rejection, leave the Error object in the array as the resolved value
            // make sure the err is wrapped in an Error object if not already an Error object
            if (!(err instanceof Error)) {
                returnVal = new Error();
                returnVal.data = err;
            }
            return returnVal;
        });
    }));
};

And, then you use it like this:

Promise.settleVal(null, [...]).then(function(results) {
    results.forEach(function(item, index) {
        if (item instanceof Error) {
            console.log("p[" + index + "] rejected with error = ", item);
        } else {
            console.log("p[" + index + "] fulfilled with value = ", item);
        }
    });
});

This can be a complete replacement for .settle() for all cases as long as an instanceof Error is never a resolved value of your promises (which it really shouldn't be).


Promise Standards Effort

As of 2019, it appears that .allSettled() is becoming the standard for this type of behavior. And, here's a polyfill:

if (!Promise.allSettled) {
    Promise.allSettled = function(promises) {
        let wrappedPromises = Array.from(promises).map(p => 
             this.resolve(p).then(
                 val => ({ state: 'fulfilled', value: val }),
                 err => ({ state: 'rejected', reason: err })
             )
        );
        return this.all(wrappedPromises);
    }
}

Usage would be like this:

let promises = [...];    // some array of promises, some of which may reject
Promise.allSettled(promises).then(results => {
    for (let r of results) {
        if (r.state === 'fulfilled') {
            console.log('fulfilled:', r.val);
        } else {
            console.log('rejected:', r.err);
        }
    }
});

Note that Promise.allSettled() itself always resolves, never rejects though subsequent .then() handlers could throw or return a rejected promise to make the whole chain reject.

As of June 2019, this not yet in the current desktop Chrome browser, but is planned for an upcoming release (e.g. later in 2019).

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Your `isPromise` function is more like an `isThenable` test? However, in `settle` a real `instanceof Promise` (or just always calling `Promise.resolve`) seems appropriate. – Bergi Apr 13 '16 at 17:37
  • @Bergi - Yes, it is an `isThenable()`. In the interest of efficiency, I was trying to avoid wrapping a promise in a promise when that is not needed and I was trying to be as accepting as possible for any thenable in case there are mixed types of promises floating around (such as jQuery promises or some other promise library). – jfriend00 Apr 13 '16 at 17:45
  • Actually that's exactly what `Promise.resolve(p).then(…)` does :-) If `p` already is a `Promise`, it is not wrapped. – Bergi Apr 13 '16 at 17:46
  • @Bergi - Are you sure? I thought `Promise.resolve()` would at least wrap anything that wasn't the same type of promise as `Promise.resolve()`. In fact, isn't that how you "cast" one type of promise to another and also handle plain values that were in the array? I was just trying to avoid that casting overhead since it did not seem necessary if it was already a promise. You can perhaps argue that avoiding that overhead isn't necessary. – jfriend00 Apr 13 '16 at 17:51
  • Yes, I'm sure, [the spec says so](http://www.ecma-international.org/ecma-262/6.0/#sec-promise.resolve). I thought otherwise for some time as well, where `Promise.resolve` would always wrap, but apparently the suggested `Promise.cast` became the standardised `Promise.resolve`. – Bergi Apr 13 '16 at 17:59
  • @Bergi - But that says this: ***The resolve function returns either a new promise resolved with the passed argument, or the argument itself if the argument is a promise produced by this constructor.*** which makes it sound like it will wrap a different kind of promise that isn't the kind that is "produced by this constructor' and the logic steps appear to test the constructor itself to see if it is the same. – jfriend00 Apr 13 '16 at 18:07
  • That's what I meant with "`p` already is a `Promise`". If it's some other, shady kind of promise, or even just a thenable, you'll usually *want* to wrap it. – Bergi Apr 13 '16 at 18:11
  • 2
    @Bergi - I updated the code to just always call `Promise.resolve()` and remove the `isPromise()` call. Definitely simpler. – jfriend00 Apr 14 '16 at 00:38
  • 1
    @Elliot - I added a somewhat simpler option that can be used 90% of the time. – jfriend00 Apr 15 '16 at 02:44
  • My question is why is Settle not part of the standard? Is it an oversight of much needed functionality, or is this rarely needed or perhaps even some kind of anti-pattern? – Chris_F Dec 23 '18 at 01:09
  • @Chris_F - It's not an antipattern. Sometimes you just want to know when all requests are done and get all results, success or not. I don't know why it's not standard. – jfriend00 Dec 23 '18 at 01:21
  • Added section at the end of the answer that discusses `.allSettled()` which is the proposed Promise standard for settle-like behavior. – jfriend00 Jun 21 '19 at 16:08
  • @Chris_F - There now is a proposed standard for settle-like behavior (added to the end of the answer). – jfriend00 Jun 21 '19 at 16:09
  • Your polyfill isn't 100% compliant due to [steps 1 and 2](https://tc39.es/proposal-promise-allSettled/#sec-promise.allsettled). You should change `Promise.resolve(p)` and `Promise.all(wrappedPromises)` to `this.resolve(p)` and `this.all(wrappedPromises)` so that it will throw a `TypeError` if it is not called from a `Promise` constructor. You also need to `Array.from(promises)` to handle iterables other than arrays. – Patrick Roberts Jun 21 '19 at 16:20
  • @PatrickRoberts - OK, I made those changes. I don't understand the "if not called from a promise constructor" part of your comment. `Promise.allSettled()` is not something that is called from a Promise constructor. – jfriend00 Jun 21 '19 at 16:31
  • @jfriend00 `Promise.allSettled(...);` and `const { allSettled } = Promise; allSettled.call(Promise, ...);` are both called from a Promise constructor. `const { allSettled } = Promise; allSettled(...);` is _not_ called from a Promise constructor, so it should throw a `TypeError` according to the specification. – Patrick Roberts Jun 21 '19 at 16:33
  • It just occurred to me why you misunderstood what I meant. `Promise` is a constructor. I'm not saying that `allSettled()` is called from within a `new Promise(...)` invocation, I'm just saying its calling context (`this`) must be a valid Promise constructor. Whether it's native or a polyfill should not matter though, so libraries like bluebird would be able to `BluebirdPromise.allSettled = Promise.allSettled;` and a specification-conforming implementation would internally use the BluebirdPromise's implementation instead of the native promise's implementation as you initially had your polyfill. – Patrick Roberts Jun 21 '19 at 16:40
  • @PatrickRoberts - OK makes sense now. Thx. – jfriend00 Jun 21 '19 at 17:03