等待所有承诺完成,即使有一些被拒绝

576

假设我有一组正在进行网络请求的Promise,其中一个将会失败:

// 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没有提供这样的方法,那么在不使用任何 promises 库的情况下,推荐使用什么模式来处理此问题?


承诺被拒绝后,应该在结果数组中返回什么? - Kuba Wyrostek
10
ES6 的 Promise 不支持这个方法(目前看起来比 Bluebird 更慢)。此外,并不是所有的浏览器或引擎都已经支持它们。我强烈建议使用 Bluebird,该库带有 allSettled 方法,可以满足你的需求,而无需自己编写代码实现。 - Dan
1
除了Dan分享的内容,bluebird库中类似于allSettled / settleAll的功能可以通过"reflect"函数来实现。 - user3344977
2
@Coli:嗯,我不这么认为。Promise.all 会在 任何一个 promise 被拒绝时立即拒绝,因此您提出的习语不能保证所有 promise 都已解决。 - Jörg W Mittag
我认为值得分享这里提供的解决方案 - 使用async/await非常干净 - https://dev59.com/Tl0a5IYBdhLWcg3waYCM#46024590 - does_not_compute
显示剩余4条评论
20个回答

470

更新,您可能想要使用内置的本地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");
});

3
我认为这是一个很好的解决方案。你能否修改一下,使得语法更简单易懂?问题的关键在于,如果你想在子Promise中处理错误,你应该捕获并返回错误。例如:https://gist.github.com/nhagen/a1d36b39977822c224b8 - Nathan Hagen
4
它让你能够确定哪些被拒绝和哪些被满足,并将问题提取到可重用的操作符中。 - Benjamin Gruenbaum
5
为了解决自己的问题,我创建了以下npm包:https://github.com/Bucabug/promise-reflect https://www.npmjs.com/package/promise-reflect - SamF
2
我一段时间前遇到了这个问题,为此我创建了这个npm包:https://www.npmjs.com/package/promise-all-soft-fail - velocity_distance
5
“reflect”这个词在计算机科学中常用吗?你能否提供一个类似维基百科的链接来解释它。我一直在搜索“Promise.all not even first reject”,但不知道要搜索“Reflect”。ES6是否应该有一个“Promise.reflect”,类似于“Promise.all”,但真正意义上包含所有内容? - Noitidart
显示剩余8条评论

285

类似的答案,但对于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:")等)。


2
@KarlBateman 我觉得你有点混淆了。这里解决或拒绝函数的顺序并不重要,因为 .map(p => p.catch(e => e)) 部分将所有拒绝转换为已解决的值,所以 Promise.all 仍然会等待所有东西完成,无论个别函数是解决还是拒绝,无论它们需要多长时间。试一下。 - jib
45
因为这段代码永远不会失败,所以.catch(e => console.log(e));从未被调用。 - fregante
5
没问题。虽然使用catch来结束Promise链通常是个好习惯 在我看来 - jib
3
它捕获错误 e 并将其作为常规值(成功值)返回。与 p.catch(function(e) { return e; }) 相同,只是更短。return 是隐式的。 - jib
1
@jib 我认为在这种情况下,有很大的风险会让不太熟悉 Promise 工作原理的开发人员产生混淆,以为 p.catch(e => e) 仍然会拒绝 Promise,因为它返回一个 Error,尽管实际上它是解决了这个 Error - Andy
显示剩余10条评论

87

Benjamin的回答提供了一个很好的抽象方法来解决这个问题,但我希望有一个不太抽象的解决方案。明确的解决方法是在内部Promise上简单地调用.catch,并从它们的回调函数中返回错误。

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

进一步地,你可以编写一个通用异常捕获处理程序,它看起来像这样:

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!' ]

为了遵循DRY原则,你需要采用Benjamin的回答:

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以保持一致性。
我认为我的解决方案是显式和简单,但确实不够健壮。该接口无法保证您确切地知道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!' ]
在我的情况下,我不需要知道错误或它是如何失败的 - 我只关心我是否拥有该值。我会让生成承诺的函数担心记录特定的错误。
const apiMethod = () => fetch()
  .catch(error => {
    console.log(error.message);
    throw error;
  });

这样,应用程序的其余部分如果希望可以忽略其错误,并在需要时将其视为未定义值。

我希望我的高级函数能够安全地失败,而不必担心为什么它的依赖项失败,当我需要做出权衡时,我更喜欢KISS而不是DRY -- 这就是我选择不使用reflect的最终原因。


1
@Benjamin,我认为@Nathan的解决方案对于Promise非常简单和惯用。虽然你的reflect提高了代码重用性,但它也建立了另一个抽象层次。由于迄今为止,与您相比,Nathan的答案只收到了一小部分赞同票,我想知道这是否表明他的解决方案存在问题,而我尚未意识到。 - user6445533
2
@LUH3417 这个解决方案在概念上不太可靠,因为它将错误视为值,并没有将错误与非错误分开。例如,如果其中一个 Promise 合法地解析为可以抛出的值(这是完全可能的),那么这种情况会出现严重问题。 - Benjamin Gruenbaum
2
@BenjaminGruenbaum 所以举个例子,new Promise((res, rej) => res(new Error('合法错误')))new Promise(((res, rej) => rej(new Error('非法错误'))) 是无法区分的吗?或者更进一步地说,您将无法通过 x.status 进行过滤吗?我会在我的答案中添加这一点,以便区别更加明显。 - Nathan Hagen
3
这个想法不好的原因是它将Promise实现和仅在特定Promise.all()变体中使用的具体用例绑定在一起,这也使得承诺的消费者必须知道特定的Promise不会拒绝,但会吞噬其错误。事实上,reflect()方法可以通过将其称为PromiseEvery(promises).then(...)来使其更少抽象且更明确。与Benjamin的答案相比,上面的答案的复杂性应该能说明这个解决方案的问题。 - Neil

45

现在有一个已经完成的提案,可以在纯JavaScript中原生实现这个功能:Promise.allSettled。它已经到达了第四阶段,在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"
  }
]

对于旧版浏览器,可以使用符合规范的兼容性补丁程序这里


1
它是第4阶段,预计将在ES2020中实现。 - Estus Flask
1
也可在 Node 12 中使用 :) - Callum M
3
即使其他答案仍然有效,由于这是解决此问题的最新方法,因此这个答案应该得到更多的赞同票。 - Jacob
1
@CertainPerformance 在使用Promise.allSettled时,使用“catch error”有意义吗?谢谢。 - Ricardo

10

我真的很喜欢Benjamin的回答,以及他如何将所有Promise基本上转化为总是解决但有时会以错误作为结果的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”。在bluebird中也有这个,我更喜欢reflect,但对于数组的情况,这是一个可行的解决方案。 - Benjamin Gruenbaum
2
好的,“settle”确实是一个更好的名称。 :) - Kuba Wyrostek
这看起来很像显式 Promise 构造反模式。需要注意的是,您永远不应该自己编写此类函数,而应使用库提供的函数(好吧,原生 ES6 有点贫乏)。 - Bergi
请问您能否正确使用Promise构造函数(并避免使用那个var resolve的东西)? - Bergi
非常感谢,这正是我正在寻找的 :-) Promise 构造函数具有无与伦比的优势,超越了 deferreds,它可以捕获异常(例如当 promises 不是我们所期望的内容时)。 - Bergi
显示剩余5条评论

5
var err;
Promise.all([
    promiseOne().catch(function(error) { err = error;}),
    promiseTwo().catch(function(error) { err = error;})
]).then(function() {
    if (err) {
        throw err;
    }
});
Promise.all会吞掉任何被拒绝的Promise并将错误存储在变量中,因此它将在所有Promise都已解析时返回。然后您可以重新抛出错误或执行其他操作。通过这种方式,我猜您会得到最后一个拒绝而不是第一个。

1
似乎可以通过将其转换为数组并使用 err.push(error) 来聚合错误,以便所有错误都可以被传递上来。 - ps2goat

4

我曾经遇到过同样的问题,并通过以下方式解决了它:

const fetch = (url) => {
  return node-fetch(url)
    .then(result => result.json())
    .catch((e) => {
      return new Promise((resolve) => setTimeout(() => resolve(fetch(url)), timeout));
    });
};

tasks = [fetch(url1), fetch(url2) ....];

Promise.all(tasks).then(......)

在这种情况下,Promise.all将等待每个Promise进入resolvedrejected状态。
有了这个解决方案,我们以非阻塞的方式“停止catch执行”。事实上,我们并没有停止任何东西,我们只是以未决状态返回Promise,当超时后它会返回另一个Promise

但是当你运行Promise.all时,它会立即调用所有的承诺。我正在寻找一种方法来监听所有承诺都已被调用的情况,但不要自己调用它们。谢谢。 - SudoPlz
@SudoPlz 方法 all() 就是这样,它等待所有 Promise 的履行或至少一个 Promise 的拒绝。 - user1016265
这是正确的,但它不仅仅是等待,它实际上会调用/启动/触发进程。如果您希望在其他地方启动承诺,那是不可能的,因为.all会启动所有内容。 - SudoPlz
@SudoPlz 希望这能改变你的看法 https://jsfiddle.net/d1z1vey5/ - user1016265
3
我改变了观点。之前我认为Promise只有在被调用(比如使用then.all方法)时才会运行,但实际上它们在创建时就已经开始运行了。 - SudoPlz

3
这应该与Q库的做法一致: Promise.allSettled()
if(!Promise.allSettled) {
    Promise.allSettled = function (promises) {
        return Promise.all(promises.map(p => Promise.resolve(p).then(v => ({
            state: 'fulfilled',
            value: v,
        }), r => ({
            state: 'rejected',
            reason: r,
        }))));
    };
}

3

不要拒绝,使用一个对象来解决它。 当你实现Promise时,可以这样做:

const promise = arg => {
  return new Promise((resolve, reject) => {
      setTimeout(() => {
        try{
          if(arg != 2)
            return resolve({success: true, data: arg});
          else
            throw new Error(arg)
        }catch(e){
          return resolve({success: false, error: e, data: arg})
        }
      }, 1000);
  })
}

Promise.all([1,2,3,4,5].map(e => promise(e))).then(d => console.log(d))


1
这看起来是一个不错的解决方法,虽然不够优雅但能够工作。 - Sunny Tambi

2
本杰明·格鲁恩鲍姆的回答当然很好。但我也能看到纳森·哈根观点的抽象级别似乎有些模糊。像e&v这样的短对象属性也没有帮助,但当然可以更改。
在Javascript中,有一个标准的Error对象,称为Error。理想情况下,您总是会抛出此类的实例/后代。优点是您可以执行instanceof Error,并且您知道某些东西是错误的。
因此,基本上捕获错误,如果错误不是Error类型,则将错误包装在Error对象内。结果数组将具有已解决的值或您可以检查的Error对象。
catch中的instanceof是为了防止您使用了可能执行reject("error")而不是reject(new Error("error"))的外部库。
当然,您可以拥有承诺,您解决了一个错误,但在这种情况下,最好仍将其视为错误,就像最后一个示例所示的那样。
这样做的另一个优点是使数组解构保持简单。
const [value1, value2] = PromiseAllCatch(promises);
if (!(value1 instanceof Error)) console.log(value1);

最初的回答

不要使用

const [{v: value1, e: error1}, {v: value2, e: error2}] = Promise.all(reflect..
if (!error1) { console.log(value1); }

你可以认为!error1检查比instanceof更简单,但你也必须解构v & e两个对象。最初的回答:您可以争论说!error1检查比instanceof更简单,但您还必须解构v&e两个对象。

function PromiseAllCatch(promises) {
  return Promise.all(promises.map(async m => {
    try {
      return await m;
    } catch(e) {
      if (e instanceof Error) return e;
      return new Error(e);
    }
  }));
}


async function test() {
  const ret = await PromiseAllCatch([
    (async () => "this is fine")(),
    (async () => {throw new Error("oops")})(),
    (async () => "this is ok")(),
    (async () => {throw "Still an error";})(),
    (async () => new Error("resolved Error"))(),
  ]);
  console.log(ret);
  console.log(ret.map(r =>
    r instanceof Error ? "error" : "ok"
    ).join(" : ")); 
}

test();


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接