Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

AngularJs and Promises with the $http Service


:P
On this page:
continued from Page 1

When using the $http service with Angular I’ve often wondered why the $http service opts to use a custom Promise instance that has extension methods for .success() and .error(). rather than relying on the more standard .then() function to handle the callbacks. Traditional promises (using the $q Service in Angular) have a .then() function to provide a continuation on success or failure, and .then() receives parameters for a success and failure callback. The various $http.XXXX functions however, typically use the .success() and .error() functions to handle callbacks. Underneath the $http callbacks there is still a $q Promise, but the extension functions abstract away some of the ugliness that is internal to the $http service.

This might explain that when looking at samples of Angular code that use the $http service inside of custom services, I often see code that creates a new wrapper Promise and returns that back to the caller rather than the original $http Promise.

The idea is simple enough – you want to create a service that captures the data and stores is and then notify the controller that the data has changed or refreshed. Let’s look at a few different approaches to help us understand how the $http service works with its custom promises.

Let’s look at a simple example (also on Plunker). Assume you have a small HTML block with an ng-repeat that displays some data:

<div class="container" ng-controller="albumsController as view" style="padding: 20px;">
    <ul class="list-group">
        <li class="list-group-item" ng-repeat="album in view.albums">
        {{album.albumName}} <i class="">{{album.year}}</i>
    </ul>
</div> 

You then implement a service to get the data via $http and a controller that can use the data that the service provides.

The Verbose Way

Let’s start with the more complex verbose way of creating an extra promise which seems to be a commonly used pattern I’ve seen in a number of examples (including a number of online training courses). I want to start with this because it nicely describes the common usage pattern for creating custom Promises in JavaScript.

Here’s what this looks like in an factory Angular service implementation:

app.factory('albumService', ['$http', '$q'],
    function albumService($http, $q) {
        // interface
        var service = {
            albums: [],
            getAlbums: getAlbums
        };
        return service;

        // implementation
        function getAlbums() {
            var def = $q.defer();

            $http.get("./albums.ms")
                .success(function(data) {
                    service.albums = data;
                    def.resolve(data);
                })
                .error(function() {
                    def.reject("Failed to get albums");
                });
            return def.promise;
        }
    });

The code in getAlbums() creates an initial Deferred object using $q.defer(). Then the $http.get() is called and when the initial $http callback returns either .resolve() or .reject() is called on the deferred instance. When – later on in the future – the HTTP call returns it triggers the Deferred to fire its continuation callbacks to the success or failure operations on whoever is listening to the promise on the promise .then()  function. Before the callback comes back though the at this point unresolved Promise is first returned back to the caller which in this case is the controller  that’s calling this service function.

The calling controller can now capture the service result by attaching to the resulting promise like this:

app.controller('albumsController', [
    '$scope', 'albumService',
    function albumsController($scope, albumService) {
        var vm = this;
        vm.albums = [];

        vm.getAlbums = function() {
            albumService.getAlbums()
                .then(function(albums) {
                    vm.albums = albums;
                    console.log('albums returned to controller.');
                },
                function(data) {
                    console.log('albums retrieval failed.')
                });
        };
        
        vm.getAlbums();
    }
]);

Now when the HTTP call succeeds (or fails), it come back to the $http.get().success or error functions which in turn triggers the wrapper Promise, which then in turn fires the .then() in the controller with either result data (success) or an http error object (error).

When you run this, the Controller’s view .albums property is updated, which is in turn causes the list of albums to render in the browser.

Sweet it works. But – the use of the extra deferred is code that you can do without in most cases.

##AD##

$http functions already return Promises

The $http functions  already return a Promise object themselves. This means there’s really very little need to create a new deferred and pass the associated promise back, much less having to handle the resolving and rejecting code as part of your service logic. Using the extra Promise to me would make sense only if you actually need to return something different than what the $http call is returning and you can’t chain the promise.

Promises can be chained, meaning you can have multiple listeners on a single Promise. So the service is one listener as it handles its .success and .error calls, but you can also pass that promise back to the caller and it can also receive a callback on that same Promise – after the service callback has fired.

Using the raw $http Promise as a result, the previous service getAlbums() function could be re-written a bit simpler like this:

function getAlbumsSimple() {
    return $http.get("albums.js")
        .success(function(data) {
            service.albums = data;
        });
}

This code simply captures the data from the service which is the albums JSON collection and assigns it to the service properties. The actual result from the call is a Promise instance and that is returned. Notice that the service here doesn’t handle any errors – that’s actually deferred to the client which may have to display some error information in the UI. If you wanted to pre-process error information you’d implement the error handler here and set something like an object on the service.

The controller can now consume this service method simply like this:

vm.getAlbumsSimple = function() {
    albumService.getAlbumsSimple()
        .success(function(albums) {            
            vm.albums = albums;
            console.log('albums returned to controller.', vm.albums);
        })
        .error(function() {
            console.log('albums retrieval failed.');
        });
};

using the same familiar .success() and .error() functions that are used on the original $http functions.

The code is similar to the original .then() Controller example, except that you are using .success() and .error() instead of .then(). This provides the albums collection to the .success() callback and we our albums assigned it works just fine.

This works because promises can be chained and have multiple listeners. Promises are guaranteed that the callbacks are called in the order that they are attached and so the service function gets the first crack,  and then the controller function gets called after that. Both get notified and both can now respond off the single Promise instance.

However, the downside of this approach is that you have to know that the service is returning you an $http promise that has .success() and .error() functions which is kind of … non-standard.

What about $http.XXX.then()?

You can also still use the .then() function on an $http.XXX function call, but the behavior changes slightly from the original call. Here’s is the same controller code written with the .then() function:

vm.getAlbumsSimple = function() {
    albumService.getAlbumsSimple()
        .then(function (httpData) {
            vm.albums = httpData.data;
        },function(httpData) {
            console.log('albums retrieval failed.');
        });
};

Unfortunately the .then() function receives a somewhat different parameter signature than the .success and .error calls do. Now a top level data object is returned in the success callback of .then() and the actual result data that is attached to the .data property of that object. The object also contains other information about the $http request.

Here’s what the actual object looks like:

$httpThen

The object holds some HTTP request data like the headers, status code and status text, which is useful on failures. And it has a .data member that holds the actual request data that you’re interested in. Hence you need to do:

vm.albums = httpData.data;

inside of the .then() callback to get at the data. This is not quite what you’d expect and I suspect one of the reasons why so many people use a wrapper promise to hide this complex object and return the data directly as part of the wrapper Promise .then() call.

$http.then() Error Callback

When using .then() with an $http call and when an error occurs you get the same object returned to you, but now the data member contains the raw HTTP response from the server rather than parsed result data. Here’s the httpData object from the error callback function parameter:

$httpThenError

It’s nice that the error callback returns the raw HTTP response – if you’re calling a REST service and it returns a 500 error result, but also a valid error JSON response you can potentially take action and parse the error into something that’s usable on the client. That’s a nice touch.

$http.error() Callback

Since we’re speaking of error callbacks lets also look at the .error() callback parameters. The error callback has a completely different parameter and object layout than the .then() error callback which is unfortunate. Here’s an example of the signature in the $http.XXX.error() function:

albumService.getAlbumsSimple()
    .success(function(httpData) {
        vm.albums = httpData.data;
    })
    .error(function (http, status, fnc, httpObj ) {        
        console.log('albums retrieval failed.',http,status,httpObj);
    });

The error callback receives parameters for the full HTTP response, a status code, and an http object that looks like this:

$httpError

Seems pretty crazy that the Angular team chose a completely different parameter signature on this error function compared to .then(). The signature here is similar to jQuery’s and I suspect that’s why this was done, although the httpObj has its own custom structure. Essentially it looks like the .then() method should be considered an internal function with .success() and .error() being the public interface. Again very unfortunate as this breaks the typical expectation of promises that use .then() for code continuation and expect a single data result object on success calls.

To be fair though the data that is contained in these result parameters is very complete and it does allow you build good error messages assuming the server returns decent error information in the right (JSON) format for you to do something with. Inconsistent - yes, but at least it’s complete!

$http Inconsistency

I find it a bit frustrating that Angular chose to create the $http methods with custom Promises that are in effect behaving differently than stock promises. By implementing .success() and .error() $http is effectively hiding some of the underlying details of the raw promise that is fired on the HTTP request. Even worse is that .then() is essentially behaving like an internal function rather than the public interface. Clearly the intent by the Angular team was to have consumers use .success() and .error() rather than .then().

##AD##

This behavior provides some additional abilities to do this but it seems very counter intuitive and inconsistent. It seems like it would have been a much better choice to allow the .then() method to work the same as .success() and .error() with the same parameter signatures and adding extra parameters for the additional data that might be needed internally. Or even not have .success() and .error() altogether and have .then() just return the same values that those methods return to be consistent with the way promises are used elsewhere in Angular and in JavaScript in general.

This inconsistency and the fact that the .then() data object exposes $http transport details likely explains why so many people are wrapping the $http promises into another promise in order to provide a consistent promise result returned to the caller so that your usage of promises is consistent in your application. It just seems this would have been nice to do at the actual framework level in the first place.

Extending Promises to support $http Promises

If you use $http Promises in your Angular Services you may find that from time to time you need to return some data conditionally either based on an HTTP call, or from data that is cached. For example imagine a service that needs to return a cached resource rather than a resource retrieved from $http (note this code doesn’t work!):

function getAlbums(noCache) {

    // if albums exist just return
    if (!noCache && service.albums && service.albums.length > 0) {
        var def = $q.defer();
        def.resolve(service.albums);        
        return def.promise;
    }
    
    return $http.get("../api/albums/")
        .success(function (data) {                    
            service.albums = data;                   
        })
        .error(onPageError);
}

The code that returns the cached albums needs to return a promise rather than the actual data because the $http call also returns a promise. So there’s some extra work that needs to be done to return this data. But – the above code actually would fail, because an $http promise expects .success() and .error() methods that traditional promises are missing.

In effect you need to extend the traditional promise to support the $http promise functions. To help with this I created a couple of helper functions:

(function(undefined) {
    ww = {};
    var self;
    ww.angular = {
        // extends deferred with $http compatible .success and .error functions
        $httpDeferredExtender: function(deferred) {
            deferred.promise.success = function(fn) {
                deferred.promise.then(fn, null);
                return deferred.promise;
            }
            deferred.promise.error = function(fn) {
                deferred.promise.then(null, fn);
                return deferred.promise;
            }
            return deferred;
        },
        // creates a resolved/rejected promise from a value
        $httpPromiseFromValue: function($q, val, reject) {
            var def = $q.defer();
            if (reject)
                def.reject(val);
            else
                def.resolve(val);
            self.$httpDeferredExtender(def);
            return def.promise;
        }
    };
    self = ww.angular;
})();

.$httpDeferredExtender() takes a traditional promise and turns it into an $http compatible promise, so that it has .success() and .error() methods to assign to.

Using this extender you can get the code to work like this:

function getAlbums(noCache) {
    // if albums exist just return
    if (!noCache && service.albums && service.albums.length > 0) {
        var def = $q.defer();
        def.resolve(service.albums);
        ww.angular.$httpDeferredExtender(def);
        return def.promise;
    }

    return $http.get("../api/albums/")
        .success(function (data) {                    
            service.albums = data;                   
        })
        .error(onPageError);
}

To make this even simpler the second helper directly creates a resolved or rejected promise:

function getAlbums(noCache) {

    if (!noCache && service.albums && service.albums.length > 0) 
        return ww.angular.$httpPromiseFromValue($q, service.albums);
        
    return $http.get("../api/albums/")
        .success(function (data) {                    
            service.albums = data;                   
        })
        .error(onPageError);
}

This really makes it easy to return cached values consistently back to the client.

Summary

Personally I’ve resigned myself to simply forwarding the $http generated Promises and using .success() and .error() at the cost of a little bit of inconsistency. At this point I have to know that this particular call in my service returns an $http promise, and that I need to call the .success() and .error() functions on it rather than .then() to handle the callbacks rather. But I still prefer that to wrapping my services with extra Promises. Regardless of where you push this behavior, somewhere in the stack you end up having this inconsistenty where the difference between $http promises and stock Promises shows up – so I might as well push it up into the application layer and save some senseless coding to hide an implementation detail.

I definitely don’t like the alternative or wrapping every service $http call into a wrapper promise since that’s tedious and painful to read in the service and adds another indirection call to every service call. But I guess it depends how much you value consistency – maybe it’s worth it to you to have the extra layer but treat every Promise in your application the same way using .then() syntax.

I’ve written this up mainly to help me remember all the different ways that results and errors are returned – I have a feeling I’ll find myself coming back to this page frequently to ‘remember’. Hopefully some of you find this useful as well.

Resources

Posted in Angular  JavaScript  

The Voices of Reason


 

Jim
October 25, 2014

# re: AngularJs and Promises with the $http Service

Nice article Rick... We decided to go the AngularJs route.. I do love ExtJS4 but it's not free and licensing is confusing.... I'll still use jquery grids...

John B
November 13, 2014

# re: AngularJs and Promises with the $http Service

Internally, the success() and error() functions are just thin wrappers around the then() function, corresponding to the successCallback and errorCallback functions of the promise. Perhaps I'm wrong in this thought, but I've always considered them to be "helper" functions to make processing HTTP responses easier, rather than requirements for use.

The only benefit I see to using success() and error() is that they include the original config, which then() does not. If you don't need that, then I wouldn't (and haven't) bothered using them.

Rick Strahl
November 13, 2014

# re: AngularJs and Promises with the $http Service

@John - no I think you have that right. The reason is that .then() receives the message object, rather than results in its success handler which is ugly and I'm guessing that's why this was done. I really wish though that .then() handlers would work like the .success()/.error() handlers and maybe have an additional parameter for the message object rather than these helpers that make it very inconsistent to return the promise.

One scenario where this matters is when you cache data for example. Say you want to make an $http call to get the data, but return cached data instead of calling the $http handler if the data is already local. Now you have to create a custom promise adding .success() and .error() methods in order to return your custom promise() for the cached data which is pretty ugly.

I've created a helper function that basically lets me turn any promise() into a $http compatible promise to make this work for more easily for me so I have one call to make that work. It's not exactly clean but at least that helps keep service output consistent.

It's all pretty minor in the big picture of features, but is something I've stumbled with a few times and I see others running into this all the time as well.

Thijs
December 05, 2014

# re: AngularJs and Promises with the $http Service

Nice article! "Unreachable code detected" in the first example. It's actually returning the http promise instead of the deferred one. The script on plunker is correct.

Rick Strahl
December 10, 2014

# re: AngularJs and Promises with the $http Service

@Thijs - thanks. Fixed in the article.

Hardik
April 09, 2015

# re: AngularJs and Promises with the $http Service

Thanks for the perfect explaination, it clears out lot of fundamentals of promise in angularjs.

Anish George
September 11, 2015

# re: AngularJs and Promises with the $http Service

Great Article man. Exactly what I was looking for. Thanks for the explanation on how to transform a regular promise into an $http compatible one.

Steven
September 26, 2015

# re: AngularJs and Promises with the $http Service

I cannot thank you enough. This is exactly what I was looking for. I just started learning AngularJS and spent hours looking for this.

André
September 30, 2015

# re: AngularJs and Promises with the $http Service

Thanks for this, I was struggling about two days with proper json retrieval, until I found this article. Also love how you structured it.
Works a treat.

kaibingz
March 09, 2017

# re: AngularJs and Promises with the $http Service

Great article. Exactly the answer I was looking for. Thank you so much!


Charm
April 05, 2017

# re: AngularJs and Promises with the $http Service

This article really helps! And 100% working fine!


Daniel
June 22, 2017

# re: AngularJs and Promises with the $http Service

Thank you so much for your article. I'm just getting started with Angular, though I've been an ASP.NET developer for 15 years. I'm trying out your code, and I'm getting an error on the "$http.get" line in the service call. The error is "Object doesn't support property or method 'success'". I'm using the simple example from Plunker. Here's the code:

var PeopleApp = angular.module('peopleApp', []);

PeopleApp.controller('peopleController', ['$scope', 'peopleService',
    function ($scope, peopleService) {

    getPeople();
    function getPeople() {
        peopleService.getPeople()
            .then(function(ppl) {
                $scope.people = ppl;
                console.log($scope.people);
            })
            (function() {
                $scope.status = 'Unable to load people data: ' + error.message;
                console.log($scope.status);
            });
    }
}]);

PeopleApp.factory('peopleService', ['$http', function ($http) {

    var peopleService = {
        people: [],
        getPeople: getPeople
    };
    return peopleService;

    function getPeople() {
        return $http.get('/People/GetJsonPeople')
            .success(function(data) {
                PeopleService.getPeople = data;
            });
    };

}]);

I'm hoping you (or someone else) could tell me what's awry. I've verified that the call to "GetJsonPeople" is working. (This is in my ASP.NET MVC controller.)


Suvaberata
August 19, 2017

# re: AngularJs and Promises with the $http Service

Deprecation Notice

The $http legacy promise methods .success and .error have been deprecated and will be removed in v1.6.0. Use the standard .then method instead.

— AngularJS (v1.5) $http Service API Reference -- Deprecation Notice.


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024