Search code examples
javascriptpromisees6-promise

Wait until all promises complete even if some rejected


Let's say I have a set of Promises that are making network requests, of which one will fail:

const arr = [ fetch('index.html'), fetch('http://does-not-exist') ]

Promise.all(arr)
  .then(res => console.log('success', res))
  .catch(err => console.log('error', err)) // This is executed   

Let's say I want to wait until all of these have finished, regardless of if one has failed. There might be a network error for a resource that I can live without, but which if I can get, I want before I proceed. I want to handle network failures gracefully.

Since Promise.all doesn't leave any room for this, what is the recommended pattern for handling this, without using a promises library?


Solution

  • Benjamin's answer offers a great abstraction for solving this issue, but I was hoping for a less abstracted solution. The explicit way to to resolve this issue is to simply call .catch on the internal promises, and return the error from their callback.

    let a = new Promise((res, rej) => res('Resolved!')),
        b = new Promise((res, rej) => rej('Rejected!')),
        c = a.catch(e => { console.log('"a" failed.'); return e; }),
        d = b.catch(e => { console.log('"b" failed.'); return e; });
    
    Promise.all([c, d])
      .then(result => console.log('Then', result)) // Then ["Resolved!", "Rejected!"]
      .catch(err => console.log('Catch', err));
    
    Promise.all([a.catch(e => e), b.catch(e => e)])
      .then(result => console.log('Then', result)) // Then ["Resolved!", "Rejected!"]
      .catch(err => console.log('Catch', err));
    

    Taking this one step further, you could write a generic catch handler that looks like this:

    const catchHandler = error => ({ payload: error, resolved: false });
    

    then you can do

    > Promise.all([a, b].map(promise => promise.catch(catchHandler))
        .then(results => console.log(results))
        .catch(() => console.log('Promise.all failed'))
    < [ 'Resolved!',  { payload: Promise, resolved: false } ]
    

    The problem with this is that the caught values will have a different interface than the non-caught values, so to clean this up you might do something like:

    const successHandler = result => ({ payload: result, resolved: true });
    

    So now you can do this:

    > Promise.all([a, b].map(result => result.then(successHandler).catch(catchHandler))
        .then(results => console.log(results.filter(result => result.resolved))
        .catch(() => console.log('Promise.all failed'))
    < [ 'Resolved!' ]
    

    Then to keep it DRY, you get to Benjamin's answer:

    const reflect = promise => promise
      .then(successHandler)
      .catch(catchHander)
    

    where it now looks like

    > Promise.all([a, b].map(result => result.then(successHandler).catch(catchHandler))
        .then(results => console.log(results.filter(result => result.resolved))
        .catch(() => console.log('Promise.all failed'))
    < [ 'Resolved!' ]
    

    The benefits of the second solution are that its abstracted and DRY. The downside is you have more code, and you have to remember to reflect all your promises to make things consistent.

    I would characterize my solution as explicit and KISS, but indeed less robust. The interface doesn't guarantee that you know exactly whether the promise succeeded or failed.

    For example you might have this:

    const a = Promise.resolve(new Error('Not beaking, just bad'));
    const b = Promise.reject(new Error('This actually didnt work'));
    

    This won't get caught by a.catch, so

    > Promise.all([a, b].map(promise => promise.catch(e => e))
        .then(results => console.log(results))
    < [ Error, Error ]
    

    There's no way to tell which one was fatal and which was wasn't. If that's important then you're going to want to enforce and interface that tracks whether it was successful or not (which reflect does).

    If you just want to handle errors gracefully, then you can just treat errors as undefined values:

    > Promise.all([a.catch(() => undefined), b.catch(() => undefined)])
        .then((results) => console.log('Known values: ', results.filter(x => typeof x !== 'undefined')))
    < [ 'Resolved!' ]
    

    In my case, I don't need to know the error or how it failed--I just care whether I have the value or not. I'll let the function that generates the promise worry about logging the specific error.

    const apiMethod = () => fetch()
      .catch(error => {
        console.log(error.message);
        throw error;
      });
    

    That way, the rest of the application can ignore its error if it wants, and treat it as an undefined value if it wants.

    I want my high level functions to fail safely and not worry about the details on why its dependencies failed, and I also prefer KISS to DRY when I have to make that tradeoff--which is ultimately why I opted to not use reflect.