Как я могу ожидать нескольких promises в параллельном режиме без "неудачного" поведения?

Я использую async/await для параллельного вызова нескольких api:

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

Я знаю, что, в отличие от loops, Promise.all выполняется в параллельном режиме (то есть часть ожидающего результата находится параллельно).

Но Я также знаю, что:

Promise.all отклоняется, если один из элементов отклонен и Promise.all не работает быстро: если у вас есть четыре promises, которые разрешаются после тайм-аут, и один немедленно отклоняется, тогда Promise.all отклоняет немедленно.

Как я читал, если я Promise.all с 5 promises, а первый для завершения возвращает a reject(), то остальные 4 будут эффективно отменены и их обещанные значения resolve() будут потеряны.

Есть ли третий способ? Где выполнение эффективно параллельно, но один отказ не портит всю связку?

Ответ 1

Использование catch означает, что обещание разрешается (если вы не выбросите исключение из catch или вручную отклоните цепочку обещаний), поэтому вам не нужно явно возвращать разрешенные обещания IIUC.

Это означает, что просто обрабатывая ошибки с помощью catch, вы можете достичь того, что хотите.

Если вы хотите стандартизировать способ обработки отклонения, вы можете применить функцию обработки отклонения ко всем promises.

async function bar() {
    await new Promise(r=> setTimeout(r, 1000))
      .then(()=> console.log('bar'))
      .then(()=> 'bar result');
}
async function bam() {
    await new Promise((ignore, reject)=> setTimeout(reject, 2000))
      .catch(()=> { console.log('bam errored'); throw 'bam'; });
}
async function bat() {
    await new Promise(r=> setTimeout(r, 3000))
      .then(()=> console.log('bat'))
      .then(()=> 'bat result');
}

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

async function foo(arr) {
  console.log('foo');
  return await Promise.all([bar(), bam(), bat()].map(handleRejection));
}

foo().then(results=> console.log('done', results));

Ответ 2

В то время как техника в принятом ответе может решить вашу проблему, это анти-шаблон. Решение обещания с ошибкой не является хорошей практикой, и есть более чистый способ сделать это.

То, что вы хотите сделать, это псевдоязык:

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

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

Это можно сделать просто с помощью async/await без использования Promise.all. Рабочий пример:

console.clear();

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

/** 
 * This will be runned in series, because 
 * we call a function and immediately wait for it 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');