ES6 Promise.all() 错误句柄 - 是否需要 .settle()?

ES6 Promise.all() error handle - Is .settle() needed?

假设我有一个 Promise.all() 处理两个承诺。如果一个 promise 产生了错误,但另一个 promise 解决了,我希望能够在 Promise.all() 解决后根据情况处理错误。

ES6 Promise 缺少 settle 方法,我认为这是有充分理由的。但我忍不住认为 .settle() 方法会让我更容易解决这个问题。

我是在以错误的方式解决这个问题,还是在此处使用 settle 方法扩展 ES6 Promises 是正确的做法?

我如何考虑使用 .settle() 的示例:

Promise.all([Action1,Action2])
.settle(function(arrayOfSettledValues) 
    //if 1 failed but not 2, handle
    //if 2 failed but not 1, handle
    //etc....
)

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

您不能直接使用 Promise.all() 来生成 .settle() 类型的行为,无论是否拒绝,都可以获得所有结果,因为 Promise.all() 是 "fast-fail" 并且 returns 一旦第一个承诺拒绝并且只有 returns 拒绝原因,none 其他结果。

因此,需要一些不同的东西。通常,解决该问题的最简单方法是向创建您的承诺数组的任何操作添加一个 .then() 处理程序,以便它捕获任何拒绝并将它们变成具有您可以测试的特定值的已履行承诺.但是,这种类型的解决方案取决于实现,因为它完全取决于您要 return 的价值类型,因此这并不完全通用。

如果您想要一个通用的解决方案,那么 .settle() 之类的东西非常有用。

您不能使用结构:

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

注(2019 年添加):Promise 标准工作似乎已选择 Promise.allSettled() 作为 "settle-like" 行为的标准实现。您可以在本答案的末尾看到更多相关信息。

因为 Promise.all() 在您传递的第一个承诺被拒绝时拒绝,并且 return 只是那个拒绝。 .settle() 逻辑如下:

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

而且,如果您有兴趣,这里有一个相当简单的 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);
        });
    }));
}

在此实现中,Promise.settle() 将始终解析(从不拒绝)并且它解析为一个 PromiseInspection 对象数组,这允许您测试每个单独的结果以查看它是已解析还是已拒绝,并且每个的价值或原因是什么。它的工作原理是将 .then() 处理程序附加到传入的每个承诺,处理该承诺的解决或拒绝,并将结果放入 PromiseInspection 对象,然后成为承诺的已解决值。

然后您将像这样使用此实现;

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());
        }
    });
});

仅供参考,我自己编写了另一个版本的 .settle,我称之为 .settleVal(),当您不需要实际的拒绝原因时,我经常发现它更容易使用,您只需要知道给定的数组槽是否被拒绝。在此版本中,您传入一个默认值,该值应替换任何被拒绝的承诺。然后,您只得到一个平面数组值 returned 和任何设置为默认值的值都会被拒绝。例如,您通常可以从 null0""{} 中选择一个 rejectVal,这会使结果更易于处理。这是函数:

// 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;
        });
    }));
};

然后,你可以这样使用它:

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

这并不是 .settle() 的完整替代品,因为有时您可能想知道它被拒绝的真正原因,或者您无法轻易地区分被拒绝的值和未被拒绝的值。但是,我发现 90% 以上的时间,这更易于使用。


这是我对 .settle() 的最新简化,它在 return 数组中留下一个 instanceof Error 作为区分已解决的值和被拒绝的错误的方法:

// 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;
        });
    }));
};

然后,你可以这样使用它:

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);
        }
    });
});

这可以完全替代 .settle() 在所有情况下,只要 instanceof Error 永远不是您承诺的已解决值(它确实不应该是)。


承诺标准努力

截至 2019 年,.allSettled() 似乎正在成为此类行为的标准。而且,这是一个 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);
    }
}

用法是这样的:

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);
        }
    }
});

请注意,Promise.allSettled() 本身总是解析,从不拒绝,尽管随后的 .then() 处理程序可能会抛出或 return 一个被拒绝的承诺以使整个链被拒绝。

截至 2019 年 6 月,当前桌面 Chrome 浏览器中尚未出现此功能,但计划在即将发布的版本中发布(例如 2019 年晚些时候)。