3

I have a function which makes web service calls to my server and returns an array of promises.

However some of these calls might work while others might not. The way my function is currently set up, if one of them fails, it alerts that the entire thing failed. If I'm making 5 calls, 1 might fail. I need to log this correctly and I'm not sure how to do it.

An ideal response/log would be:

  1. call 1 passed
  2. call 2 passed
  3. call 3 passed
  4. call 4 failed - reason
  5. call 5 passed

Right now the whole thing will return "The handle user operation failed" because call 4 failed.

Function:

var manageGroup = function (add, group, users){

    var deffered = $q.defer();
    var arrPromises = [];
    var promiseIndex = arrPromises.length;
    var usersLength = users.length;
    var operation = add ? "AddUserToGroup" : "RemoveUserFromGroup";
    var actionText = add ? "Added: " : "Removed: "
    var actionText2 = add ? " to " : " from "

    //Apply operation on selected groups
    for (var i = 0; i < usersLength; i++){
        arrPromises[i] = $().SPServices({
            operation: operation,
            groupName: group.name,
            userLoginName: users[i].domain
        });      
    }

    $q.all(arrPromises).then(
        function (){
            //when promises are finsihed
            for (var i = 0; i < usersLength; i++){
                console.log(actionText + users[i].name + actionText2  + group.name);
            };
            deffered.resolve();
        },
        //function incase of AJAX failure
        function (){
            alert('The handle user operation failed.');
        }
    ) 
    return deffered.promise;      
}

I tried to handle the promises individually instead of using the $q.all but now I'm not getting anything in the log:

I took this section out:

/*$q.all(arrPromises).then(
    function (){
        //when promises are finsihed
        for (var i = 0; i < usersLength; i++){
            console.log(actionText + users[i].name + actionText2  + group.name);
        };
        deferred.resolve();
    },
    //function incase of AJAX failure
    function (){
        alert('The handle user operation failed.');
    }
) */

Introduced this instead:

for (var i = 0; i<promiseIndex; i++){
    arrPromises[i].then(
        function (){
            console.log(actionText + user[i].name + actionText2 + group.name);
        }
    ),
    function (){
        alert('Failed to add/remove'+  user[i].name + ' to ' + group.name)
    }
}

$q.all(arrPromises).then(function (){
    deferred.resolve();
}, function (){
    deferred.reject();
})
Batman
  • 5,563
  • 18
  • 79
  • 155
  • You're not using jQuery's Deferred, do you? What is `$q.all`, Angular's Q promise implementation? – Bergi Feb 05 '14 at 16:58
  • Yes my mistake, it's angular's implementation of promises. I'm updated the tag. – Batman Feb 05 '14 at 17:00
  • possible duplicate of [AngularJS: $q wait for all even when 1 rejected](http://stackoverflow.com/questions/18888104/angularjs-q-wait-for-all-even-when-1-rejected) – Bergi Feb 05 '14 at 17:02
  • Not a duplicate from what I can tell. He's talking about waiting for all promises to resolve. I'm asking how to I handle each promise individually. – Batman Feb 05 '14 at 17:29
  • Q and bluebird give a method called `allSettled` or `settle`. It gives you exactly that: once all the promises are finished (resolved or rejected), it's called with a list of these promises, and their state. – Florian Margaine Feb 05 '14 at 17:38

3 Answers3

5

Q (on which ng.$q is based on) or bluebird have a method fulfilling exactly your needs.

For bluebird, you'd go this way:

var Promise = require('bluebird');

Promise.settle(arrPromises).then(function(promises) {
    promises.forEach(function(p) {
        if (promise.isRejected()) {
            // it's a rejected promise.
        }
        else {
            // it's a resolved promise.
        }
    });
});

And for Q, you'd go this way:

var Q = require('q');

Q.allSettled(arrPromises).then(function(promises) {
    promises.forEach(function(p) {
        if (p.state === 'fulfilled') {
            // it's a resolved promise.
        }
        else {
            // it's a rejected promise.
        }
    });
});

The nice thing about both these libraries is that they're compliant with the Promises/A+ specification. Which means you can take off ng.$q, put one of these, and your current code will still work.

Florian Margaine
  • 58,730
  • 15
  • 91
  • 116
  • Hmm, this looks really simple to use. – Batman Feb 05 '14 at 17:51
  • @Batman yes, that's the point. Both of these libraries recognized that it can be a common use case and offered a method to handle this. If you start working with promises a lot, you'll see that these libraries offer a lot more, like `bind` or `spread` which can be invaluable. – Florian Margaine Feb 05 '14 at 17:53
4

It seems that you're having an issue with one ajax call failure causing an overall failure of all(). You could just catch the failure of the individual AJAX calls and resolve the corresponding promise with a value of your choosing. Here, I am just using an empty string.

Live demo (click).

Keep in mind, this is just sample code to demonstrate the point.

  //store promises to use for all()
  var promises = [];

  //loop for ajax calls
  for (var i=0; i<3; ++i) {
    //new deferred for each call
    var deferred = $q.defer();
    //cache the promise
    promises[i] = deferred.promise;
    //use another function to avoid unwanted variable increment
    makeCall(deferred, i);
  }

  $q.all(promises).then(function(allData) {
    console.log(allData);
  });

  function makeCall(deferred, i) {
    //make the ajax call
    $http.get('file'+i+'.txt').then(function(resp) {
      console.log('Call '+i+' returned.');
        //resolve the promise with ajax data if successful
        deferred.resolve(resp.data);
    }, function() {
      //resolve with something else on failure
      deferred.resolve('');
    });
  }
m59
  • 43,214
  • 14
  • 119
  • 136
  • I don't understand the purpose of the $q.all(promises).then(function(allData)) – Batman Feb 08 '14 at 00:37
  • @Batman do you not want to wait for all the calls to be finished? – m59 Feb 08 '14 at 00:41
  • Oh yea I definitely do. Like I'd love to log every single transaction pass or fail and ur code does that really well. And I'm trying to get it to give me an end result at the end like "All transactions successful" or "Operation completed with errors" but the .all is catching the first error and resolving, which calls my .final function right after the first error. Here's how I did it: http://jsfiddle.net/r98Vb/ – Batman Feb 08 '14 at 00:45
  • You can see what I mean here: http://i.imgur.com/zRIvAxi.png The highlighted log should have been called at the very end of the operation but is being called much too early. Also, I can't pass in my resolve or rejected parameters into .finally...is this normal? – Batman Feb 08 '14 at 00:48
  • @Batman I don't have time to debug your code. Everything you need to know is in my answer. – m59 Feb 08 '14 at 00:50
  • I was able to get it to work by extending $q http://stackoverflow.com/questions/18888104/angularjs-q-wait-for-all-even-when-1-rejected otherwise ur code worked great. Thanks. – Batman Feb 08 '14 at 01:21
  • @Batman whatever makes you happy, but as you can see in my demo, that's not necessary. – m59 Feb 08 '14 at 01:22
  • @yea you're right. I'm not sure why but I can't seem to reproduce it for some reason. – Batman Feb 08 '14 at 01:48
  • @Batman no wonder!! You're rejecting the promises on call failure, so of course `all()` fails. That's the same problem you already had. See the notes in my code. You need to resolve the promise Whether it succeeds or fails. What my solution does is gives you another abstraction - you resolve the promise used in `all()` from within the rejection of the ajax promise. Instead, you rejected it there. – m59 Feb 08 '14 at 01:52
  • Yea you're right, your solution works perfectly without having to extend $q – Batman Feb 09 '14 at 21:03
1

I tried to handle the promises individually instead of using the $q.all but now I'm not getting anything in the log

You seem to have fallen for the classical closure-in-a-loop problem, with your i variable having the wrong value when the callback is executed. Instead, use this:

for (var i = 0; i<promiseIndex; i++) (function(i) {
    arrPromises[i].then(function() {
        console.log(actionText + user[i].name + actionText2 + group.name);
    }, function( ){
        alert('Failed to add/remove'+  user[i].name + ' to ' + group.name)
    });
})(i);

Also you had mismatched parenthesis in the then call, essentially not passing the error handler.

Now, each promise is handled individually; yet their order is not persisted. For that, you will need to use some kind of all, have a look at @Florian's answer.


Also notice that there's no reason to use that deffered explicitly. Just return $q.all(arrPromises)! Manually resolving deferreds and returning their promise is cumbersome and error-prone - in your original code you simply forgot to reject it in case of an error. Don't use this when you already have promises and can use combinators on them.

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375