This question already has answers here: Wait until all promises complete even if some rejected (20 answers) Closed 3 years ago.
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?
catch
), thereby avoiding the fast-fail. Would this do what you want?
const noop = function(){}
Promise.all( arr.map( v => doAsyncThing(v).catch(noop) ) )
turns an error into an undefined-value
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?
reject()
ed then end up in the results array
as undefined
?
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.
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.
return new Promise
so that we could get the correct results array: jsbin.com/ruralujame/edit?html,css,js,console,output
Success story sharing
parallel
function is an antipattern that will potentially cause unhandled rejections.task1
rejects beforetask2
fulfills, you still are failing fast.then
is anti-pattern because Promises has a dedicated way of dealing with errors in thecatch
branches so developers expect values inthen
and errors incatch
. Mixing that up will lead to confusion for others who are not aware of your custom implementation details.