javascriptasynchronouspromiseasync-await

How do I await multiple promises in-parallel without 'fail-fast' behavior?


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?


Solution

  • 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.