140

I'm using async/await to fire several api calls in parallel:

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

I know that, unlike loops, Promise.all executes in-parallel (that is, the waiting-for-results portion is in parallel).

But I also know that:

Promise.all is rejected if one of the elements is rejected and Promise.all fails fast: If you have four promises which resolve after a timeout, and one rejects immediately, then Promise.all rejects immediately.

As I read this, if I Promise.all with 5 promises, and the first one to finish returns a reject(), then the other 4 are effectively cancelled and their promised resolve() values are lost.

Is there a third way? Where execution is effectively in-parallel, but a single failure doesn't spoil the whole bunch?

Community
  • 1
  • 1
Brandon
  • 7,736
  • 9
  • 47
  • 72
  • The other four are not cancelled, but their results are not propagated along the promise chain (IIUC). – Ben Aston Dec 22 '16 at 22:16
  • 1
    If you want to avoid a specific failure mode rejecting the promise chain, then you can handle that failure in the sub-promise chain (using `catch`), thereby avoiding the fast-fail. Would this do what you want? – Ben Aston Dec 22 '16 at 22:17
  • 1
    `const noop = function(){}` `Promise.all( arr.map( v => doAsyncThing(v).catch(noop) ) )` turns an error into an undefined-value – Thomas Dec 22 '16 at 22:22
  • @BenAston do you mean instead of `return doAsyncThing(v)`, `return doAsyncThing(v).catch(err => {return err})` ? ETA: I'm not sure what should go in the `catch` body--is there a way to pass the error object up w/o `reject`? So long as I can handle the `reject()`ed promises individually and still get the values from the `resolve()`d promises. re: `cancelled` vs `not propagated`, i am not certain I understand the difference. Both result in lost `resolve()` values, yeah? – Brandon Dec 22 '16 at 22:29
  • @Thomas will the promises that were `reject()`ed then end up in the results `array` as `undefined`? – Brandon Dec 22 '16 at 22:30
  • yes. and if you write `.catch(err => {return err})` the `err` will end up in the results array. Do you expect a 1:1 mapping from src to results-Array or do you only want the resolved values (in order)? How do you want to handle the rejected promises? ignore them? convert them into some (default-)Value? handle them by a callback-function? ... – Thomas Dec 22 '16 at 22:48
  • for this particular use case I'll just be ignoring & counting the failures (eg, with `statsd`) for metrics purposes, so the 1:1 is not critical. however, I'm finding that `Promise.all` is a pretty common pattern in `async`/`await`, so I expect in the near future I'll want to handle them one-at-a-time. – Brandon Dec 22 '16 at 23:18

2 Answers2

155

While the technique in the accepted answer can solve your issue, it's an anti-pattern. Resolving a promise with an error isn't good practice and there is a cleaner way of doing this.

What you want to do, in pseudo-code, is:

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

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

This can be achieved simply with async/await without the need to use Promise.all. A working example:

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

Note: You will need a browser which has async/await enabled to run this snippet.

This way you can use simply try/ catch to handle your errors, and return partial results inside parallel function.

jarmod
  • 71,565
  • 16
  • 115
  • 122
NoNameProvided
  • 8,608
  • 9
  • 40
  • 68
  • 5
    "*anti-pattern: resolving a promise with an error isn't good practice*" - where did you get that from? No, on the contrary, [your `parallel` function is an antipattern](https://stackoverflow.com/questions/46889290/waiting-for-more-than-one-concurrent-await-operation) that will potentially cause unhandled rejections. – Bergi Aug 30 '18 at 13:24
  • 3
    And what's worse: this doesn't even solve the OPs problem. If `task1` rejects before `task2` fulfills, you still are failing fast. – Bergi Aug 30 '18 at 13:25
  • 1
    _where did you get that from?_ - Handling errors in `then` is anti-pattern because Promises has a dedicated way of dealing with errors in the `catch` branches so developers expect values in `then` and errors in `catch`. Mixing that up will lead to confusion for others who are not aware of your custom implementation details. – NoNameProvided Aug 30 '18 at 14:27
  • 1
    _your parallel function is an antipattern_ - if you read my answer you can find it in the bottom how to deal with that: _"This way you can use simply try/ catch to handle your errors, and return partial results inside the parallel function."_ This is the answer for your second comment as well: _And what's worse: this doesn't even solve the OPs problem._ – NoNameProvided Aug 30 '18 at 14:27
  • 1
    I am with @Bergi on this one (but you could be a bit less harsh pal :D ). After reading the [link](https://stackoverflow.com/questions/46889290/waiting-for-more-than-one-concurrent-await-operation) provided seems clear to me that the Promise.all technique is more concise, and allows to get the error in the moment one of the promises reject. Maybe it is true that you can use try/catch inside the Parallel function, but everything gets more cumbersome in my humble opinion. Peace folks! – John Bernardsson Aug 07 '19 at 08:03
  • 1
    I would like to see the implementation with `try/catch` as this doesn't behave as expected. I want both results to be settled, but when one fails, the function doesn't even return due to `Unhandled Error`. – Qwerty Aug 07 '19 at 16:58
  • 3
    I downvoted because this answer does not make explicit the solution to the propagation of errors. – Ben Aston Feb 27 '20 at 13:34
  • @NoNameProvided Why not just `await Promise.all([wait(500, 'a'), wait(500, 'b')])` in `taskRunner(label)` for parallel? – s3c Sep 01 '22 at 19:31
  • It seems that in your initial code block `fn task()` you need to add `await` before result-1... to result-n as: `return handleResults(await result-1, ...)` – S.Serpooshan Mar 19 '23 at 20:36
97

ES2020 contains Promise.allSettled, which will do what you want.

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

Output:

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

But if you want to "roll your own", then you can leverage the fact that using Promise#catch means that the promise resolves (unless you throw an exception from the catch or manually reject the promise chain), so you do not need to explicitly return a resolved promise.

So, by simply handling errors with catch, you can achieve what you want.

Note that if you want the errors to be visible in the result, you will have to decide on a convention for surfacing them.

You can apply a rejection handling function to each promise in a collection using Array#map, and use Promise.all to wait for all of them to complete.

Example

The following should print out:

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

See also.

Ben Aston
  • 53,718
  • 65
  • 205
  • 331
  • 11
    return await is redundant fyi https://eslint.org/docs/rules/no-return-await – Peter Berg Sep 27 '17 at 13:48
  • 1
    Why do you even use .then when you already use await? You could just remove all .then calls and it would work. – FINDarkside Apr 29 '18 at 14:47
  • I can’t remember. I think I was trying to elucidate. – Ben Aston Apr 29 '18 at 16:41
  • 1
    the output of your script does not include 'bar result' and 'bat result', but: done [ undefined, { "error": "bam" }, undefined ] is that intended? – David Schumann Aug 30 '18 at 09:23
  • 2
    Ya, I believe it should `return new Promise` so that we could get the correct results array: https://jsbin.com/ruralujame/edit?html,css,js,console,output – Arvin Sep 09 '18 at 17:14
  • 1
    adding to David's comment, done [ undefined, { "error": "bam" }, undefined ] is returned. If you add return await and a then for bam, the results are displayed [Log] done – ["bar result", "bam result", "bat result"]. However I am unable to get the result done – ["bar result", {"error":"bam"}, "bat result"]. Is there a way to achieve this ? – Phanikiran Apr 19 '19 at 20:10
  • This answer is actually very misleading on how promises and async/await work. – Piou Sep 06 '19 at 15:53