Wait until all promises complete even if some rejected

前端 未结 18 1914
醉酒成梦
醉酒成梦 2020-11-21 04:55

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

// http://does-not-exist will throw a TypeError
va         


        
相关标签:
18条回答
  • 2020-11-21 05:30

    Here's my custom settledPromiseAll()

    const settledPromiseAll = function(promisesArray) {
      var savedError;
    
      const saveFirstError = function(error) {
        if (!savedError) savedError = error;
      };
      const handleErrors = function(value) {
        return Promise.resolve(value).catch(saveFirstError);
      };
      const allSettled = Promise.all(promisesArray.map(handleErrors));
    
      return allSettled.then(function(resolvedPromises) {
        if (savedError) throw savedError;
        return resolvedPromises;
      });
    };
    

    Compared to Promise.all

    • If all promises are resolved, it performs exactly as the standard one.

    • If one of more promises are rejected, it returns the first one rejected much the same as the standard one but unlike it waits for all promises to resolve/reject.

    For the brave we could change Promise.all():

    (function() {
      var stdAll = Promise.all;
    
      Promise.all = function(values, wait) {
        if(!wait)
          return stdAll.call(Promise, values);
    
        return settledPromiseAll(values);
      }
    })();
    

    CAREFUL. In general we never change built-ins, as it might break other unrelated JS libraries or clash with future changes to JS standards.

    My settledPromiseall is backward compatible with Promise.all and extends its functionality.

    People who are developing standards -- why not include this to a new Promise standard?

    0 讨论(0)
  • 2020-11-21 05:31

    Similar answer, but more idiomatic for ES6 perhaps:

    const a = Promise.resolve(1);
    const b = Promise.reject(new Error(2));
    const c = Promise.resolve(3);
    
    Promise.all([a, b, c].map(p => p.catch(e => e)))
      .then(results => console.log(results)) // 1,Error: 2,3
      .catch(e => console.log(e));
    
    
    const console = { log: msg => div.innerHTML += msg + "<br>"};
    <div id="div"></div>

    Depending on the type(s) of values returned, errors can often be distinguished easily enough (e.g. use undefined for "don't care", typeof for plain non-object values, result.message, result.toString().startsWith("Error:") etc.)

    0 讨论(0)
  • 2020-11-21 05:33

    Benjamin Gruenbaum answer is of course great,. But I can also see were Nathan Hagen point of view with the level of abstraction seem vague. Having short object properties like e & v don't help either, but of course that could be changed.

    In Javascript there is standard Error object, called Error,. Ideally you always throw an instance / descendant of this. The advantage is that you can do instanceof Error, and you know something is an error.

    So using this idea, here is my take on the problem.

    Basically catch the error, if the error is not of type Error, wrap the error inside an Error object. The resulting array will have either resolved values, or Error objects you can check on.

    The instanceof inside the catch, is in case you use some external library that maybe did reject("error"), instead of reject(new Error("error")).

    Of course you could have promises were you resolve an error, but in that case it would most likely make sense to treat as an error anyway, like the last example shows.

    Another advantage of doing it this, array destructing is kept simple.

    const [value1, value2] = PromiseAllCatch(promises);
    if (!(value1 instanceof Error)) console.log(value1);
    

    Instead of

    const [{v: value1, e: error1}, {v: value2, e: error2}] = Promise.all(reflect..
    if (!error1) { console.log(value1); }
    

    You could argue that the !error1 check is simpler than an instanceof, but your also having to destruct both v & e.

    function PromiseAllCatch(promises) {
      return Promise.all(promises.map(async m => {
        try {
          return await m;
        } catch(e) {
          if (e instanceof Error) return e;
          return new Error(e);
        }
      }));
    }
    
    
    async function test() {
      const ret = await PromiseAllCatch([
        (async () => "this is fine")(),
        (async () => {throw new Error("oops")})(),
        (async () => "this is ok")(),
        (async () => {throw "Still an error";})(),
        (async () => new Error("resolved Error"))(),
      ]);
      console.log(ret);
      console.log(ret.map(r =>
        r instanceof Error ? "error" : "ok"
        ).join(" : ")); 
    }
    
    test();

    0 讨论(0)
  • 2020-11-21 05:35

    Update, you probably want to use the built-in native Promise.allSettled:

    Promise.allSettled([promise]).then(([result]) => {
       //reach here regardless
       // {status: "fulfilled", value: 33}
    });
    

    As a fun fact, this answer below was prior art in adding that method to the language :]


    Sure, you just need a reflect:

    const reflect = p => p.then(v => ({v, status: "fulfilled" }),
                                e => ({e, status: "rejected" }));
    
    reflect(promise).then((v => {
        console.log(v.status);
    });
    

    Or with ES5:

    function reflect(promise){
        return promise.then(function(v){ return {v:v, status: "fulfilled" }},
                            function(e){ return {e:e, status: "rejected" }});
    }
    
    
    reflect(promise).then(function(v){
        console.log(v.status);
    });
    

    Or in your example:

    var arr = [ fetch('index.html'), fetch('http://does-not-exist') ]
    
    Promise.all(arr.map(reflect)).then(function(results){
        var success = results.filter(x => x.status === "fulfilled");
    });
    
    0 讨论(0)
  • 2020-11-21 05:36

    I don't know which promise library you are using, but most have something like allSettled.

    Edit: Ok since you want to use plain ES6 without external libraries, there is no such method.

    In other words: You have to loop over your promises manually and resolve a new combined promise as soon as all promises are settled.

    0 讨论(0)
  • 2020-11-21 05:38

    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.

    0 讨论(0)
提交回复
热议问题