即使有些承诺被拒绝,也要等到所有的承诺都完成

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

// 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没有为此留出任何空间,那么在不使用承诺库的情况下,处理这个问题的推荐模式是什么呢?

255184 次浏览

我不知道你在使用哪个承诺库,但大多数都有类似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");
});
我真的很喜欢本杰明的回答,以及他如何基本上把所有的承诺都变成了总是能解决,但有时会导致错误的承诺。:) < br > 这是我对你的要求的尝试,以防你正在寻找替代方案。此方法简单地将错误视为有效结果,编码类似于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);
});
}

本杰明的回答为解决这个问题提供了一个很好的抽象,但我希望有一个不那么抽象的解决方案。解决这个问题的显式方法是简单地在内部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));

更进一步,你可以写一个通用的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的。缺点是你有更多的代码,你必须记住反映你所有的承诺,使事情一致。

我将把我的解决方案描述为explicit和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将接受任何被拒绝的承诺,并将错误存储在一个变量中,因此当所有承诺都已解决时,它将返回。然后可以重新抛出错误,或者做其他事情。这样,我猜你会得到最后一次拒绝,而不是第一次。

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

Depending on the type(s) of values returned, errors can often be distinguished easily enough (e.g. use undefined for "don't care", typeof for plain non-object values, result.message, result.toString().startsWith("Error:") etc.)

我会这样做:

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

这应该与Q是怎么做的一致:

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按顺序执行您的逻辑。它将在每个承诺上暂停,等待解决/拒绝,并将解决的结果分配给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状态。

有了这个解决方案,我们以非阻塞的方式“停止catch执行”。事实上,我们并没有停止任何事情,我们只是返回处于挂起状态的Promise,当它在超时后被解析时,它会返回另一个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中有一个被拒绝,它返回第一个被拒绝的promise,与标准的promise大致相同,但不同的是,它等待所有的promise都被解决/拒绝。

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

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


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


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

< / em > < em >小心。一般来说,我们从不改变内置程序,因为这可能会破坏其他不相关的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
我知道这个问题有很多答案,我确信一定(如果不是全部)是正确的。 然而,我很难理解这些答案的逻辑/流程

因此,我查看了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;
}
< p >解释:< br > —循环输入promisesList并执行每个Promise.
-无论承诺是否被解决或拒绝:根据index将承诺的结果保存在result数组中。同时保存解析/拒绝状态(isSuccess) —当所有Promise完成后,返回一个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的回答当然很好。但我也能看出内森哈根的观点在抽象层面上显得模糊。使用像e & v这样的短对象属性也没有帮助,但当然这可以改变。

在Javascript中有一个标准的错误对象,叫做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

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

有一个完成提案的函数可以在原生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"
}
]

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

不是拒绝,而是用一个对象来解决它。 当你实现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行为的填充,因为我被锁定在比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 }对象数组。

我最近建立了一个库,可以满足你的需要。它并行执行承诺,如果一个承诺失败,进程继续,最后它返回一个包含所有结果的数组,包括错误。

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