Using success/error/finally/catch with Promises in AngularJS
Asked Answered
R

6

119

I'm using $http in AngularJs, and I'm not sure on how to use the returned promise and to handle errors.

I have this code:

$http
    .get(url)
    .success(function(data) {
        // Handle data
    })
    .error(function(data, status) {
        // Handle HTTP error
    })
    .finally(function() {
        // Execute logic independent of success/error
    })
    .catch(function(error) {
        // Catch and handle exceptions from success/error/finally functions
    });

Is this a good way to do it, or is there an easier way?

Roentgenoscope answered 9/5, 2014 at 7:54 Comment(0)
D
106

Promises are an abstraction over statements that allow us to express ourselves synchronously with asynchronous code. They represent a execution of a one time task.

They also provide exception handling, just like normal code, you can return from a promise or you can throw.

What you'd want in synchronous code is:

try{
  try{
      var res = $http.getSync("url");
      res = someProcessingOf(res);
  } catch (e) {
      console.log("Got an error!",e);
      throw e; // rethrow to not marked as handled
  }
  // do more stuff with res
} catch (e){
     // handle errors in processing or in error.
}

The promisified version is very similar:

$http.get("url").
then(someProcessingOf).
catch(function(e){
   console.log("got an error in initial processing",e);
   throw e; // rethrow to not marked as handled, 
            // in $q it's better to `return $q.reject(e)` here
}).then(function(res){
    // do more stuff
}).catch(function(e){
    // handle errors in processing or in error.
});
Doolittle answered 9/5, 2014 at 8:32 Comment(8)
How would you use success(), error() and finally() combined with catch()? Or do I have to use then(successFunction, errorFunction).catch(exceotionHandling).then(cleanUp);Roentgenoscope
@Roentgenoscope generally, you do not want to ever use success and error (prefer .then and .catch instead, you can (and should) omit the errorFunction from the .then use a ccatch like in my code above).Doolittle
@BenjaminGruenbaum could you elaborate why you suggest to avoid success/error? Also my Eclipse runs amok when it sees the .catch(, so I use ["catch"]( for now. How can I tame Eclipse?Befall
Angular's $http module implementation of the $q library uses .success and .error instead of .then and .catch. However in my tests I could access all properties of the $http promise when using .then and .catch promises. Also see zd333's answer.Hoahoactzin
@SirBenBenji $q doesn't have .success and .error, $http returns a $q promise with the addition of the success and error handlers - however, these handlers do not chain and should generally be avoided if/when possible. In general - if you have questions it is best to ask them as a new question and not as a comment on an old one.Doolittle
This was a direct reply to @Giszmo's question. You orobably missed that ;).Hoahoactzin
@SirBenBenji Oh, sorry you are absolutely right - my bad.Doolittle
Please add this to your answer: "...the success and error handlers - however, these handlers do not chain and should generally be avoided if/when possible. In general...". It would make the answer more thorough and useful, because the OP asks about .success and .error. I guess this answer covers it, but still...Marciemarcile
W
44

Forget about using success and error method.

Both methods have been deprecated in angular 1.4. Basically, the reason behind the deprecation is that they are not chainable-friendly, so to speak.

With the following example, I'll try to demonstrate what I mean about success and error being not chainable-friendly. Suppose we call an API that returns a user object with an address:

User object:

{name: 'Igor', address: 'San Francisco'}

Call to the API:

$http.get('/user')
    .success(function (user) {
        return user.address;   <---  
    })                            |  // you might expect that 'obj' is equal to the
    .then(function (obj) {   ------  // address of the user, but it is NOT

        console.log(obj); // -> {name: 'Igor', address: 'San Francisco'}
    });
};

What happened?

Because success and error return the original promise, i.e. the one returned by $http.get, the object passed to the callback of the then is the whole user object, that is to say the same input to the preceding success callback.

If we had chained two then, this would have been less confusing:

$http.get('/user')
    .then(function (user) {
        return user.address;  
    })
    .then(function (obj) {  
        console.log(obj); // -> 'San Francisco'
    });
};
Wawro answered 22/10, 2015 at 17:30 Comment(1)
Also worth noting that success and error are only added to the immediate return of the $http call (not the prototype), so if you call another promise method between them (like, you normally call return $http.get(url) wrapped in a base library, but later decide to toggle a spinner in the library call with return $http.get(url).finally(...)) then you'll no longer have those convenience methods.Coinsurance
S
40

I think the previous answers are correct, but here is another example (just a f.y.i, success() and error() are deprecated according to AngularJS Main page:

$http
    .get('http://someendpoint/maybe/returns/JSON')
    .then(function(response) {
        return response.data;
    }).catch(function(e) {
        console.log('Error: ', e);
        throw e;
    }).finally(function() {
        console.log('This finally block');
    });
Subfamily answered 3/9, 2015 at 5:13 Comment(1)
Finally doesn't return the response, to my knowledge.Findlay
T
11

What type of granularity are you looking for? You can typically get by with:

$http.get(url).then(
  //success function
  function(results) {
    //do something w/results.data
  },
  //error function
  function(err) {
    //handle error
  }
);

I've found that "finally" and "catch" are better off when chaining multiple promises.

Tripletail answered 9/5, 2014 at 7:58 Comment(5)
In your example, the error handler only handles $http errors.Doolittle
Yes, I still need to handle exceptions in the success/error functions as well. And then I need some kind of common handler (where I can set things like loading = false)Roentgenoscope
You have a brace instead of a parentheses closing off your then() call.Transfinite
This doesnt work on 404 response errors, only works on .catch() MethodComplainant
This is the correct answer for handling http errors returned to controllersGilson
P
5

In Angular $http case, the success() and error() function will have response object been unwrapped, so the callback signature would be like $http(...).success(function(data, status, headers, config))

for then(), you probably will deal with the raw response object. such as posted in AngularJS $http API document

$http({
        url: $scope.url,
        method: $scope.method,
        cache: $templateCache
    })
    .success(function(data, status) {
        $scope.status = status;
        $scope.data = data;
    })
    .error(function(data, status) {
        $scope.data = data || 'Request failed';
        $scope.status = status;
    });

The last .catch(...) will not need unless there is new error throw out in previous promise chain.

Pratte answered 15/1, 2015 at 20:57 Comment(1)
Success/Error methods are deprecated.Nevis
G
-3

I do it like Bradley Braithwaite suggests in his blog:

app
    .factory('searchService', ['$q', '$http', function($q, $http) {
        var service = {};

        service.search = function search(query) {
            // We make use of Angular's $q library to create the deferred instance
            var deferred = $q.defer();

            $http
                .get('http://localhost/v1?=q' + query)
                .success(function(data) {
                    // The promise is resolved once the HTTP call is successful.
                    deferred.resolve(data);
                })
                .error(function(reason) {
                    // The promise is rejected if there is an error with the HTTP call.
                    deferred.reject(reason);
                });

            // The promise is returned to the caller
            return deferred.promise;
        };

        return service;
    }])
    .controller('SearchController', ['$scope', 'searchService', function($scope, searchService) {
        // The search service returns a promise API
        searchService
            .search($scope.query)
            .then(function(data) {
                // This is set when the promise is resolved.
                $scope.results = data;
            })
            .catch(function(reason) {
                // This is set in the event of an error.
                $scope.error = 'There has been an error: ' + reason;
            });
    }])

Key Points:

  • The resolve function links to the .then function in our controller i.e. all is well, so we can keep our promise and resolve it.

  • The reject function links to the .catch function in our controller i.e. something went wrong, so we can’t keep our promise and need to reject it.

It is quite stable and safe and if you have other conditions to reject the promise you can always filter your data in the success function and call deferred.reject(anotherReason) with the reason of the rejection.

As Ryan Vice suggested in the comments, this may not be seen as useful unless you fiddle a bit with the response, so to speak.

Because success and error are deprecated since 1.4 maybe it is better to use the regular promise methods then and catch and transform the response within those methods and return the promise of that transformed response.

I am showing the same example with both approaches and a third in-between approach:

success and error approach (success and error return a promise of an HTTP response, so we need the help of $q to return a promise of data):

function search(query) {
  // We make use of Angular's $q library to create the deferred instance
  var deferred = $q.defer();

  $http.get('http://localhost/v1?=q' + query)
  .success(function(data,status) {
    // The promise is resolved once the HTTP call is successful.
    deferred.resolve(data);              
  })

  .error(function(reason,status) {
    // The promise is rejected if there is an error with the HTTP call.
    if(reason.error){
      deferred.reject({text:reason.error, status:status});
    }else{
      //if we don't get any answers the proxy/api will probably be down
      deferred.reject({text:'whatever', status:500});
    }
  });

  // The promise is returned to the caller
  return deferred.promise;
};

then and catch approach (this is a bit more difficult to test, because of the throw):

function search(query) {

  var promise=$http.get('http://localhost/v1?=q' + query)

  .then(function (response) {
    // The promise is resolved once the HTTP call is successful.
    return response.data;
  },function(reason) {
    // The promise is rejected if there is an error with the HTTP call.
    if(reason.statusText){
      throw reason;
    }else{
      //if we don't get any answers the proxy/api will probably be down
      throw {statusText:'Call error', status:500};
    }

  });

  return promise;
}

There is a halfway solution though (this way you can avoid the throw and anyway you'll probably need to use $q to mock the promise behavior in your tests):

function search(query) {
  // We make use of Angular's $q library to create the deferred instance
  var deferred = $q.defer();

  $http.get('http://localhost/v1?=q' + query)

  .then(function (response) {
    // The promise is resolved once the HTTP call is successful.
    deferred.resolve(response.data);
  },function(reason) {
    // The promise is rejected if there is an error with the HTTP call.
    if(reason.statusText){
      deferred.reject(reason);
    }else{
      //if we don't get any answers the proxy/api will probably be down
      deferred.reject({statusText:'Call error', status:500});
    }

  });

  // The promise is returned to the caller
  return deferred.promise;
}

Any kind of comments or corrections are welcome.

Glimmer answered 13/6, 2016 at 9:33 Comment(11)
Why would you use $q to wrap the promise in a promise. Why not just return the promise that is returned by $http.get()?Typewrite
Because success() and error() wouldn’t return a new promise as then() does. With $q we make our factory to return a promise of data instead of a promise of an HTTP response.Glimmer
you response is confusing to me so maybe i'm not explaining myself well. unless you are manipulating the response then you can simply return the promise that $http returns. see this example i just wrote: jsbin.com/belagan/edit?html,js,outputTypewrite
@RyanVice I edited my answer to explain better. I hope now my point is more clear.Glimmer
That's a good clarification but checking status for error code is unnecessary as promise will get rejected AFIKTypewrite
Good point @RyanVice, it is corrected now. Other than that which option would you prefer?Glimmer
You can check my fiddle in the comment above for the pattern I recommend but it's basically function search(query) { return $http.get('http://localhost/v1?=q' + query) }Typewrite
Unless you need to do something meaningful with the return there's no value in wrapping a promise in a promise. It'd be like catching and rethrowing an exception without doing anything. There's no value to it.Typewrite
Agreed @RyanVice but I need to treat the response in the factory in case the api is down (otherwise status is -1). My goal is to encapsulate all the call logic in the factory so that the controller doesn't have to know anything about it.Glimmer
I don't see the value. It feels unnecessary to me and I reject code reviews on my projects that use this approach but if your getting value out of it then you should use it. I've also seen a few promise in angular best practice articles calling out unnecessary wrapping as a smell.Typewrite
This is a deferred anti-pattern. Read You're Missing the Point of PromisesScheck

© 2022 - 2024 — McMap. All rights reserved.