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

Wait until all promises complete even if some rejected

假设我有一组 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 库的情况下,推荐的处理模式是什么?

我不知道您使用的是哪个 promise 库,但大多数都有类似 allSettled 的内容。

编辑:好的,既然你想使用没有外部库的普通 ES6,那么没有这样的方法。

换句话说:您必须手动循环您的承诺,并在所有承诺都解决后立即解决新的组合承诺。

更新,你可能想使用内置原生 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.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);
  });
}

Benjamin 的回答为解决这个问题提供了一个很好的抽象,但我希望有一个不那么抽象的解决方案。解决此问题的明确方法是简单地调用 .catch 内部承诺,并 return 来自回调的错误。

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

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

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。缺点是你有更多的代码,你必须记住反映你所有的承诺,使事情保持一致。

我会将我的解决方案描述为显式和 KISS,但确实不够稳健。该接口不保证您确切知道承诺是成功还是失败。

例如你可能有这个:

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.

var err;
Promise.all([
    promiseOne().catch(function(error) { err = error;}),
    promiseTwo().catch(function(error) { err = error;})
]).then(function() {
    if (err) {
        throw err;
    }
});

Promise.all 将吞下任何被拒绝的承诺并将错误存储在变量中,因此当所有承诺都已解决时它将 return。然后你可以 re-throw 排除错误,或者做任何事情。这样,我猜你会得到最后一个拒绝而不是第一个。

类似的答案,但可能更符合 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>

根据返回值的类型,通常可以很容易地区分错误(例如,对 "don't care" 使用 undefined,对普通非对象值使用 typeofresult.messageresult.toString().startsWith("Error:") 等)

我会做:

var err = [fetch('index.html').then((success) => { return Promise.resolve(success); }).catch((e) => { return Promise.resolve(e); }),
fetch('http://does-not-exist').then((success) => { return Promise.resolve(success); }).catch((e) => { return Promise.resolve(e); })];

Promise.all(err)
.then(function (res) { console.log('success', res) })
.catch(function (err) { console.log('error', err) }) //never executed

这应该与how Q does it一致:

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

您可以通过同步执行器nsynjs顺序执行您的逻辑。它会在每个 promise 上暂停,等待 resolution/rejection,然后将 resolve 的结果分配给 data 属性,或者抛出一个异常(为了处理你需要 try/catch 块) .这是一个例子:

function synchronousCode() {
    function myFetch(url) {
        try {
            return window.fetch(url).data;
        }
        catch (e) {
            return {status: 'failed:'+e};
        };
    };
    var arr=[
        myFetch("https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"),
        myFetch("https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/NONEXISTANT.js"),
        myFetch("https://ajax.NONEXISTANT123.com/ajax/libs/jquery/2.0.0/NONEXISTANT.js")
    ];
    
    console.log('array is ready:',arr[0].status,arr[1].status,arr[2].status);
};

nsynjs.run(synchronousCode,{},function(){
    console.log('done');
});
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>

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

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 状态。

并且有了这个解决方案,我们 "stopping catch execution" 以一种非阻塞的方式。事实上,我们并没有停止任何事情,我们只是返回处于挂起状态的 Promise,而 returns 另一个 Promise 在超时后得到解决。

这是我的习惯 settledPromiseAll()

const settledPromiseAll = function(promisesArray) {
  var savedError;

  const saveFirstError = function(error) {
    if (!savedError) savedError = error;
  };
  const handleErrors = function(value) {
    return Promise.resolve(value).catch(saveFirstError);
  };
  const allSettled = Promise.all(promisesArray.map(handleErrors));

  return allSettled.then(function(resolvedPromises) {
    if (savedError) throw savedError;
    return resolvedPromises;
  });
};

相比于Promise.all

  • 如果所有的 promise 都被 resolve,它的表现与标准的完全一样。

  • 如果有一个 promise 被拒绝,它 returns 第一个拒绝与标准 promises 大致相同,但与它不同的是等待所有对 resolve/reject 的 promise。

为了勇敢我们可以改变Promise.all():

(function() {
  var stdAll = Promise.all;

  Promise.all = function(values, wait) {
    if(!wait)
      return stdAll.call(Promise, values);

    return settledPromiseAll(values);
  }
})();

小心。一般来说,我们从不更改内置插件,因为它可能会破坏其他不相关的 JS 库或与未来对 JS 标准的更改发生冲突。

我的 settledPromiseall 向后兼容 Promise.all 并扩展了它的功能。

正在制定标准的人们 - 为什么不将其纳入新的 Promise 标准?

自 ES5 以来,我一直在使用以下代码。

Promise.wait = function(promiseQueue){
    if( !Array.isArray(promiseQueue) ){
        return Promise.reject('Given parameter is not an array!');
    }

    if( promiseQueue.length === 0 ){
        return Promise.resolve([]);
    }

    return new Promise((resolve, reject) =>{
        let _pQueue=[], _rQueue=[], _readyCount=false;
        promiseQueue.forEach((_promise, idx) =>{
            // Create a status info object
            _rQueue.push({rejected:false, seq:idx, result:null});
            _pQueue.push(Promise.resolve(_promise));
        });

        _pQueue.forEach((_promise, idx)=>{
            let item = _rQueue[idx];
            _promise.then(
                (result)=>{
                    item.resolved = true;
                    item.result = result;
                },
                (error)=>{
                    item.resolved = false;
                    item.result = error;
                }
            ).then(()=>{
                _readyCount++;

                if ( _rQueue.length === _readyCount ) {
                    let result = true;
                    _rQueue.forEach((item)=>{result=result&&item.resolved;});
                    (result?resolve:reject)(_rQueue);
                }
            });
        });
    });
};

用法签名就像Promise.all。主要区别在于 Promise.wait 将等待所有承诺完成其工作。

我认为下面提供了一种略有不同的方法...比较 fn_fast_fail()fn_slow_fail()...虽然后者不会失败...您可以检查一个或ab 都是 Errorthrow 的实例,如果你想让它到达 catch 块(例如 if (b instanceof Error) { throw b; }) 。见 jsfiddle.

var p1 = new Promise((resolve, reject) => { 
    setTimeout(() => resolve('p1_delayed_resolvement'), 2000); 
}); 

var p2 = new Promise((resolve, reject) => {
    reject(new Error('p2_immediate_rejection'));
});

var fn_fast_fail = async function () {
    try {
        var [a, b] = await Promise.all([p1, p2]);
        console.log(a); // "p1_delayed_resolvement"
        console.log(b); // "Error: p2_immediate_rejection"
    } catch (err) {
        console.log('ERROR:', err);
    }
}

var fn_slow_fail = async function () {
    try {
        var [a, b] = await Promise.all([
            p1.catch(error => { return error }),
            p2.catch(error => { return error })
        ]);
        console.log(a); // "p1_delayed_resolvement"
        console.log(b); // "Error: p2_immediate_rejection"
    } catch (err) {
        // we don't reach here unless you throw the error from the `try` block
        console.log('ERROR:', err);
    }
}

fn_fast_fail(); // fails immediately
fn_slow_fail(); // waits for delayed promise to resolve

我知道这个问题有很多答案,而且我确定一定(如果不是全部)是正确的。 然而,我很难理解这些答案的 logic/flow。

所以我查看了 Promise.all() 上的原始实现,并试图模仿该逻辑 - 除了在一个 Promise 失败时不停止执行之外。

  public promiseExecuteAll(promisesList: Promise<any>[] = []): Promise<{ data: any, isSuccess: boolean }[]>
  {
    let promise: Promise<{ data: any, isSuccess: boolean }[]>;

    if (promisesList.length)
    {
      const result: { data: any, isSuccess: boolean }[] = [];
      let count: number = 0;

      promise = new Promise<{ data: any, isSuccess: boolean }[]>((resolve, reject) =>
      {
        promisesList.forEach((currentPromise: Promise<any>, index: number) =>
        {
          currentPromise.then(
            (data) => // Success
            {
              result[index] = { data, isSuccess: true };
              if (promisesList.length <= ++count) { resolve(result); }
            },
            (data) => // Error
            {
              result[index] = { data, isSuccess: false };
              if (promisesList.length <= ++count) { resolve(result); }
            });
        });
      });
    }
    else
    {
      promise = Promise.resolve([]);
    }

    return promise;
  }

解释:
- 循环输入 promisesList 并执行每个 Promise。
- 无论 Promise 是 resolved 还是 rejected:根据 index 将 Promise 的结果保存在 result 数组中。同时保存 resolve/reject 状态 (isSuccess)。
- 一旦所有 Promise 完成,return 一个 Promise 与所有其他 Promise 的结果。

使用示例:

const p1 = Promise.resolve("OK");
const p2 = Promise.reject(new Error(":-("));
const p3 = Promise.resolve(1000);

promiseExecuteAll([p1, p2, p3]).then((data) => {
  data.forEach(value => console.log(`${ value.isSuccess ? 'Resolve' : 'Reject' } >> ${ value.data }`));
});

/* Output: 
Resolve >> OK
Reject >> :-(
Resolve >> 1000
*/

Benjamin Gruenbaum 的回答当然很棒。但我也可以看出,内森·哈根 (Nathan Hagen) 的观点与抽象层次似乎很模糊。像 e & v 这样的短对象属性也无济于事,但当然可以改变。

在Javascript中有一个标准的错误对象,叫做Error。理想情况下,你总是抛出它的一个实例/后代。好处是你可以做到instanceof 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

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

有一个finished proposal for a function which can accomplish this natively, in vanilla Javascript: Promise.allSettled, which has made it to stage 4, is officialized in ES2020, and is implemented in all modern environments. It is very similar to the reflect function in 。这是提案页面中的示例。以前,您必须这样做:

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"
  }
]

对于旧版浏览器,有一个 spec-compliant polyfill here

与其拒绝,不如用对象来解决。 你可以在实现 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))

Promise.all 使用现代 async/await 方法

const promise1 = //...
const promise2 = //...

const data = await Promise.all([promise1, promise2])

const dataFromPromise1 = data[0]
const dataFromPromise2 = data[1]

我只想要一个完全复制 ES2020 行为的 polyfill,因为不幸的是,我被锁定在早于 12.9 的节点版本(Promise.allSettled 出现时)。所以不管它值多少钱,这是我的版本:

const settle = (promise) => (promise instanceof Promise) ?
  promise.then(val => ({ value: val, status: "fulfilled" }),
               err => ({ reason: err, status: "rejected" })) :
  { value: promise, status: 'fulfilled' };

const allSettled = async (parr) => Promise.all(parr.map(settle));

这与 ES 版本一样处理承诺和非承诺值的混合数组。它返回与本机版本相同的 { status, value/reason } 对象数组。

我最近建立了一个库,可以满足您的需求。它并行执行 promise,如果一个失败,过程继续,最后它 returns 一个包含所有结果的数组,包括错误。

https://www.npmjs.com/package/promise-ax

我希望这对某人有帮助。

const { createPromise } = require('promise-ax');
const promiseAx = createPromise();
const promise1 = Promise.resolve(4);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, new Error("error")));
const promise3 = Promise.reject("error");
const promise4 = promiseAx.resolve(8);
const promise5 = promiseAx.reject("errorAx");
const asyncOperation = (time) => {
    return new Promise((resolve, reject) => {
        if (time < 0) {
            reject("reject");
        }
        setTimeout(() => {
            resolve(time);
        }, time);
    });
};
const promisesToMake = [promise1, promise2, promise3, promise4, promise5, asyncOperation(100)];
promiseAx.allSettled(promisesToMake).then((results) =>   results.forEach((result) => console.log(result)));
// Salida esperada:
// 4
// Error: error
// error
// 8
// errorAx
// 100