等到所有Promise完成,即使有些被拒绝

IT技术 javascript promise es6-promise
2021-01-17 21:39:30

假设我有一组Promise发出网络请求s,其中一个会失败:

// http://does-not-exist will throw a TypeError
var arr = [ fetch('index.html'), fetch('http://does-not-exist') ]

Promise.all(arr)
  .then(res => console.log('success', res))
  .catch(err => console.log('error', err)) // This is executed   

假设我想等到所有这些都完成,无论是否失败。对于我可以没有的资源,可能存在网络错误,但如果我能得到,我需要在继续之前。我想优雅地处理网络故障。

既然Promise.all没有为此留出任何空间,那么在不使用Promise库的情况下处理这个问题的推荐模式是什么?

6个回答

更新,您可能想使用内置的 native Promise.allSettled

Promise.allSettled([promise]).then(([result]) => {
   //reach here regardless
   // {status: "fulfilled", value: 33}
});

作为一个有趣的事实,下面的这个答案是将该方法添加到语言中的现有技术:]


当然,你只需要一个reflect

const reflect = p => p.then(v => ({v, status: "fulfilled" }),
                            e => ({e, status: "rejected" }));

reflect(promise).then((v => {
    console.log(v.status);
});

或者使用 ES5:

function reflect(promise){
    return promise.then(function(v){ return {v:v, status: "fulfilled" }},
                        function(e){ return {e:e, status: "rejected" }});
}


reflect(promise).then(function(v){
    console.log(v.status);
});

或者在你的例子中:

var arr = [ fetch('index.html'), fetch('http://does-not-exist') ]

Promise.all(arr.map(reflect)).then(function(results){
    var success = results.filter(x => x.status === "fulfilled");
});
我认为这是一个很好的解决方案。你能修改它以包含更简单的语法吗?问题的关键在于,如果您想处理子Promise中的错误,您应该捕获它们并返回错误。例如: gist.github.com/nhagen/a1d36b39977822c224b8
2021-03-09 21:39:30
不久前我遇到了这个问题,并为它创建了这个 npm 包:npmjs.com/package/promise-all-soft-fail
2021-03-11 21:39:30
这个词reflect是计算机科学中的常用词吗?你能不能链接到像维基百科或其他东西那样解释的地方。我一直在努力寻找Promise.all not even first reject但不知道搜索“反射”。ES6 应该有一个Promise.reflect类似于“Promise.all but really all”的东西吗?
2021-03-17 21:39:30
针对我自己的问题,我创建了以下 npm 包: github.com/Bucabug/promise-reflect npmjs.com/package/promise-reflect
2021-03-26 21:39:30
@NathanHagen 它可以让您找出拒绝的内容和完成的内容,并将问题提取到可重用的运算符。
2021-04-03 21:39:30

类似的答案,但对于 ES6 来说可能更惯用:

const a = Promise.resolve(1);
const b = Promise.reject(new Error(2));
const c = Promise.resolve(3);

Promise.all([a, b, c].map(p => p.catch(e => e)))
  .then(results => console.log(results)) // 1,Error: 2,3
  .catch(e => console.log(e));


const console = { log: msg => div.innerHTML += msg + "<br>"};
<div id="div"></div>

根据类型的值(一个或多个)返回时,往往可以区分错误很轻松了(如使用undefined了“不关心”,typeof对于普通的非对象值result.messageresult.toString().startsWith("Error:")等等)

.catch(e => console.log(e)); 永远不会被调用,因为这永远不会失败
2021-03-11 21:39:30
@bfred.it 没错。虽然终止Promise链catch通常是好的做法恕我直言
2021-03-16 21:39:30
@JustinReusnow 已经包含在评论中。终止链总是好的做法,以防您稍后添加代码。
2021-03-24 21:39:30
@KarlBateman 我认为你很困惑。订单函数在此处解决或拒绝无关紧要,因为该.map(p => p.catch(e => e))部分将所有拒绝转换为已解决的值,因此Promise.all无论单个函数解决还是拒绝,仍然等待一切完成,无论它们需要多长时间。尝试一下。
2021-03-31 21:39:30
@SuhailGupta 它捕获错误e并将其作为常规(成功)值返回。p.catch(function(e) { return e; })只是更短一样。return是隐含的。
2021-04-06 21:39:30

本杰明的回答为解决这个问题提供了一个很好的抽象,但我希望有一个不那么抽象的解决方案。解决这个问题的明确方法是简单地调用.catch内部Promise,并从他们的回调中返回错误。

let a = new Promise((res, rej) => res('Resolved!')),
    b = new Promise((res, rej) => rej('Rejected!')),
    c = a.catch(e => { console.log('"a" failed.'); return e; }),
    d = b.catch(e => { console.log('"b" failed.'); return e; });

Promise.all([c, d])
  .then(result => console.log('Then', result)) // Then ["Resolved!", "Rejected!"]
  .catch(err => console.log('Catch', err));

Promise.all([a.catch(e => e), b.catch(e => e)])
  .then(result => console.log('Then', result)) // Then ["Resolved!", "Rejected!"]
  .catch(err => console.log('Catch', err));

更进一步,您可以编写一个通用的 catch 处理程序,如下所示:

const catchHandler = error => ({ payload: error, resolved: false });

那么你可以做

> Promise.all([a, b].map(promise => promise.catch(catchHandler))
    .then(results => console.log(results))
    .catch(() => console.log('Promise.all failed'))
< [ 'Resolved!',  { payload: Promise, resolved: false } ]

问题在于捕获的值将与非捕获的值具有不同的接口,因此要清理它,您可能会执行以下操作:

const successHandler = result => ({ payload: result, resolved: true });

所以现在你可以这样做:

> Promise.all([a, b].map(result => result.then(successHandler).catch(catchHandler))
    .then(results => console.log(results.filter(result => result.resolved))
    .catch(() => console.log('Promise.all failed'))
< [ 'Resolved!' ]

然后为了保持干燥,你会得到本杰明的回答:

const reflect = promise => promise
  .then(successHandler)
  .catch(catchHander)

现在的样子

> Promise.all([a, b].map(result => result.then(successHandler).catch(catchHandler))
    .then(results => console.log(results.filter(result => result.resolved))
    .catch(() => console.log('Promise.all failed'))
< [ 'Resolved!' ]

第二种解决方案的好处是它的抽象和 DRY。缺点是你有更多的代码,你必须记住反映你所有的Promise,使事情保持一致。

我会将我的解决方案描述为明确的和 KISS,但确实不那么健壮。该接口不能保证您确切知道Promise是成功还是失败。

例如你可能有这个:

const a = Promise.resolve(new Error('Not beaking, just bad'));
const b = Promise.reject(new Error('This actually didnt work'));

这不会被 抓住a.catch,所以

> Promise.all([a, b].map(promise => promise.catch(e => e))
    .then(results => console.log(results))
< [ Error, Error ]

没有办法分辨哪个是致命的,哪个不是。如果这很重要,那么您将需要强制执行和跟踪它是否成功的接口(reflect确实如此)。

如果您只想优雅地处理错误,那么您可以将错误视为未定义的值:

> Promise.all([a.catch(() => undefined), b.catch(() => undefined)])
    .then((results) => console.log('Known values: ', results.filter(x => typeof x !== 'undefined')))
< [ 'Resolved!' ]

就我而言,我不需要知道错误或它是如何失败的——我只关心我是否拥有value。我会让生成Promise的函数担心记录特定的错误。

const apiMethod = () => fetch()
  .catch(error => {
    console.log(error.message);
    throw error;
  });

这样,应用程序的其余部分可以根据需要忽略其错误,并根据需要将其视为未定义的值。

我希望我的高级函数能够安全地失败,而不用担心它的依赖项失败的原因的细节,而且当我必须做出权衡时,我也更喜欢 KISS 而不是 DRY——这就是我最终选择不使用reflect.

这是一个糟糕的主意的原因是因为它将 Promise 实现与仅在特定Promise.all()变体中使用的特定用例联系在一起,然后 Promise 消费者也有责任知道特定的 Promise 不会拒绝但会拒绝吞下它的错误。事实上,该reflect()方法可以通过调用它来减少“抽象”和更加明确PromiseEvery(promises).then(...)。与本杰明相比,上面的答案的复杂性应该说明这个解决方案。
2021-03-17 21:39:30
@LUH3417 这个解决方案在概念上不太合理,因为它将错误视为值,并且不会将错误与非错误分开。例如,如果其中一个Promise合法地解析为一个可以抛出的值(这是完全可能的),这将非常糟糕。
2021-03-19 21:39:30
@Benjamin 我认为 @Nathan 的解决方案对于Promises来说非常简单和惯用reflect改进代码重用的同时,它还建立了另一个抽象级别。由于与您的相比,内森的回答到目前为止只获得了一小部分的支持,我想知道这是否表明他的解决方案存在问题,我还没有意识到这一点。
2021-03-25 21:39:30
@BenjaminGruenbaum 例如,new Promise((res, rej) => res(new Error('Legitimate error'))不会与new Promise(((res, rej) => rej(new Error('Illegitimate error'))? 或者更进一步,您将无法通过x.status? 我会将这一点添加到我的答案中,以便区别更清楚
2021-04-06 21:39:30

有一个可以在原生 Javascript 中完成此功能完整提案Promise.allSettled,它已进入第 4 阶段,在 ES2020 中正式化,并在所有现代环境中实现它与另一个答案中reflect功能非常相似这是提案页面上的一个示例。以前,您必须执行以下操作:

function reflect(promise) {
  return promise.then(
    (v) => {
      return { status: 'fulfilled', value: v };
    },
    (error) => {
      return { status: 'rejected', reason: error };
    }
  );
}

const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.all(promises.map(reflect));
const successfulPromises = results.filter(p => p.status === 'fulfilled');

使用Promise.allSettled代替,以上将等效于:

const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);
const successfulPromises = results.filter(p => p.status === 'fulfilled');

那些使用现代环境的人将能够在没有任何库的情况下使用这种方法在这些中,以下代码段应该可以正常运行:

Promise.allSettled([
  Promise.resolve('a'),
  Promise.reject('b')
])
  .then(console.log);

输出:

[
  {
    "status": "fulfilled",
    "value": "a"
  },
  {
    "status": "rejected",
    "reason": "b"
  }
]

对于旧的浏览器,有一个符合规范的填充工具在这里

即使其他答案仍然有效,这个答案也应该得到更多的支持,因为它是解决这个问题的最新方法。
2021-03-13 21:39:30
也可用于节点 12 :)
2021-03-21 21:39:30
@CertainPerformance 在 Promise.allSettled 中使用“捕获错误”是否有意义?谢谢
2021-03-25 21:39:30
这是第 4 阶段,应该在 ES2020 中登陆。
2021-04-07 21:39:30

我真的很喜欢本杰明的回答,以及他如何基本上把所有的Promise变成总是解决但有时会出错的结果。:)
这是我应您要求的尝试,以防万一您正在寻找替代品。此方法只是将错误视为有效结果,并且编码与Promise.all其他方式类似

Promise.settle = function(promises) {
  var results = [];
  var done = promises.length;

  return new Promise(function(resolve) {
    function tryResolve(i, v) {
      results[i] = v;
      done = done - 1;
      if (done == 0)
        resolve(results);
    }

    for (var i=0; i<promises.length; i++)
      promises[i].then(tryResolve.bind(null, i), tryResolve.bind(null, i));
    if (done == 0)
      resolve(results);
  });
}
好吧,settle确实会是一个更好的名字。:)
2021-03-09 21:39:30
Bergi,请随意更改您认为必要的答案。
2021-03-21 21:39:30
您能否Promise正确使用构造函数(并避免那种var resolve东西)?
2021-03-23 21:39:30
这通常称为settle. 我们在 bluebird 中也有这个,我喜欢更好地反映,但这是一个可行的解决方案,当你有一个数组时。
2021-03-26 21:39:30
这看起来很像显式Promise构造反模式。应该注意的是,你永远不应该自己编写这样的函数,而是使用你的库提供的函数(好吧,原生 ES6 有点微薄)。
2021-04-04 21:39:30