如何并行等待多个承诺,而不会出现“快速失败”行为?

我正在使用/并行触发几个调用:asyncawaitapi

async function foo(arr) {
  const results = await Promise.all(arr.map(v => {
     return doAsyncThing(v)
  }))
  return results
}

我知道,与 不同,并行执行(即等待结果部分是并行的)。loopsPromise.all

我也知道

如果其中一个元素被拒绝并且 Promise.all 快速失败,则 Promise.all 将被拒绝:如果您有四个承诺在超时后解析,并且其中一个立即拒绝,则 Promise.all 会立即拒绝。

当我读到这篇文章时,如果我有5个承诺,而第一个完成的承诺返回了一个,那么其他4个承诺实际上被取消了,它们的承诺值就丢失了。Promise.allreject()resolve()

还有第三种方式吗?其中执行实际上是并行的,但一次失败不会破坏整个团队?


答案 1

虽然接受的答案中的技术可以解决您的问题,但它是一种反模式。解决带有错误的承诺不是很好的做法,并且有一种更清晰的方法可以做到这一点。

在伪代码中,您要执行的操作是:

fn task() {
  result-1 = doAsync();
  result-n = doAsync();

  // handle results together
  return handleResults(result-1, ..., result-n)
}

这可以简单地实现,只需使用/无需使用。一个工作示例:asyncawaitPromise.all

console.clear();

function wait(ms, data) {
  return new Promise( resolve => setTimeout(resolve.bind(this, data), ms) );
}

/** 
 * These will be run in series, because we call
 * a function and immediately wait for each result, 
 * so this will finish in 1s.
 */
async function series() {
  return {
    result1: await wait(500, 'seriesTask1'),
    result2: await wait(500, 'seriesTask2'),
  }
}

/** 
 * While here we call the functions first,
 * then wait for the result later, so 
 * this will finish in 500ms.
 */
async function parallel() {
  const task1 = wait(500, 'parallelTask1');
  const task2 = wait(500, 'parallelTask2');

  return {
    result1: await task1,
    result2: await task2,
  }
}

async function taskRunner(fn, label) {
  const startTime = performance.now();
  console.log(`Task ${label} starting...`);
  let result = await fn();
  console.log(`Task ${label} finished in ${ Number.parseInt(performance.now() - startTime) } miliseconds with,`, result);
}

void taskRunner(series, 'series');
void taskRunner(parallel, 'parallel');

注意:您将需要一个已/启用以运行此代码段的浏览器。asyncawait

通过这种方式,您可以使用简单的/ 来处理错误,并在函数内返回部分结果。trycatchparallel


答案 2

ES2020包含Promise.allSettled,它将做你想做的事。

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

输出:

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

但是,如果你想“滚动你自己的”,那么你可以利用这样一个事实,即使用Promise#catch意味着承诺可以解析(除非你从承诺链中抛出异常或手动拒绝),所以你不需要显式返回已解析的承诺。catch

因此,通过简单地处理错误,您可以实现所需的目标。catch

请注意,如果您希望错误在结果中可见,则必须决定显示这些错误的约定。

您可以使用 Array#map 将拒绝处理函数应用于集合中的每个 promise,并使用 Promise.all 等待所有 promise 完成。

以下应打印出来:

Elapsed Time   Output

     0         started...
     1s        foo completed
     1s        bar completed
     2s        bam errored
     2s        done [
                   "foo result",
                   "bar result",
                   {
                       "error": "bam"
                   }
               ]

async function foo() {
    await new Promise((r)=>setTimeout(r,1000))
    console.log('foo completed')
    return 'foo result'
}

async function bar() {
    await new Promise((r)=>setTimeout(r,1000))
    console.log('bar completed')
    return 'bar result'
}

async function bam() {
    try {
        await new Promise((_,reject)=>setTimeout(reject,2000))
    } catch {
        console.log('bam errored')
        throw 'bam'
    }
}

function handleRejection(p) {
    return p.catch((error)=>({
        error
    }))
}

function waitForAll(...ps) {
    console.log('started...')
    return Promise.all(ps.map(handleRejection))
}

waitForAll(foo(), bar(), bam()).then(results=>console.log('done', results))

另请参见