Rick Strahl's Web Log

Wind, waves, code and everything in between...
ASP.NET • C# • HTML5 • JavaScript • AngularJs
Contact   •   Articles   •   Products   •   Support   •   Search
Ad-free experience sponsored by:
ASPOSE - the market leader of .NET and Java APIs for file formats – natively work with DOCX, XLSX, PPT, PDF, images and more

Mixing $http Promises and $q Promises for cached Data


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. In pseudo code the idea is something like this from an Angular service (this code won’t work):

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

The idea is that if the data already exists, simply return the data, if it doesn’t go get it via an $http call and return the promise that calls you back when the data arrives.

Promise me

The problem with the above code is that you can’t return both straight data and a promise from the same method if you expect to handle the result data consistently in one place.

Most likely you’d want to write your controller method using code like this:

vm.getAlbums = function() {
    albumService.getAlbums() 
        .success(function(data) {
            vm.albums = data;
        })
        .error(function(err) {
            vm.errorMessage='albums not loaded');
        });            
}

The code is expecting a promise – or even more specifically an $http specific promise which is different than a standard $q promise that Angular uses. $http object promises have .success() and .error() methods in addition to the typical .then() method of standard promises. I’ve covered this topic in some detail a few weeks back in another blog post.

So in order to return a consistent result we should return an $http compatible promise. But because of the special nature of $http promises the following code that creates a promise and resolves it also doesn’t quite 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);
}

While the code works in that it returns promise, any client that tries to hook up .success() and .error() handlers will also fail with this code. Even if the consumer decided to use .then() (which both $http and plain $q promises support) the values returned to the success and error handlers are different for the $q and $http callbacks.

So to get this to work properly you really have to return an $http compatible promise.

Some Helpers to make it Easier

Because this seems to be a common scenario that I run into, I created a couple of helpers to facilitate this scenario with a couple of helper functions that can fix up an existing deferred and/or create a new completed promise directly.

(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 an existing, 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 now get the code that manually creates a $q deferred, 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);
}

It works, but there’s a slight downside to this approach. When both the success and error handlers are hooked up two separate promises are attached. Both are called because you can attach multiple handlers to a single promise but there’s a little bit of extra overhead for the extra mapping.

Moar Simpler

Because the most common scenario for this is to actually return a resolved (or rejected) promise, an even easier .$httpPromiseFromValue() helper allows me to simply create the promise directly inside of the helper which reduces the entire code to a single line:

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 when the client code expects an $http based promise.

Related Resources


I'll be at DevIntersection in Vegas this fall giving sessions on ASP.NET Core with Angular and Localization. Thinking of coming? Use discount code STRAHL and save a few bucks. If you do be sure to stop by and say hello!

ASP.NET DevIntersection 2017. Rick Strahl Coupon Code

Posted in Angular  JavaScript  

The Voices of Reason


 

Alexander Pavlyuk
December 14, 2014

# re: Mixing $http Promises and $q Promises for cached Data

As for me I prefer the opposite approach. We usually have a lot of async stuff in our js apps. And for angular there's $q service to make promises. But $http promise is different in that it produces promises with success() ans error() "helper" functions. I strongly disagree in that this exception to common interface is helping. Maybe ng devs added those functions for beginners that often use jQuery and don't like to read docs... I always wrap $http calls in $q promises inside my services and expose the latter ones to other modules. This unification is reasonable: if any other ng developer uses my modules, he will just read that my module has a list of functions that return $q-compatible promises and that's it.

Rick Strahl
December 14, 2014

# re: Mixing $http Promises and $q Promises for cached Data

@Alexander - totally agree that $http's special interface was a bad idea in that it goes against all other promises produced in Angular or otherwise. At the very least they should have exposed the .then() method with a senisible signature, but unfortunately that's not the case (.then()'s success method returns an internal object rather than the result data).

While I agree I also think that turning everything into proper promises adds extra overhead when you do use $http as you have to wrap the existing promise into another promise (ie. another extra indirection and of course extra code) and it also goes against the standard guidance for use of $http that you have to be sure to document so there are no surprises.

Torgeir
December 15, 2014

# re: Mixing $http Promises and $q Promises for cached Data

Can't you just do something like the following:

var myPromise = null;

function getAlbums() {
if (myPromise == null)
myPromise = $http.get("/someUrl").then(function (res) {
return res.data;
});
return myPromise;
}

Generally I try to avoid resolving promises in the service, but for caching it does make sense.

Rick Strahl
December 16, 2014

# re: Mixing $http Promises and $q Promises for cached Data

@Torgeir - interesting approach and yes that would work I suppose. Never thought about caching the actual Promise instance... Wonder if that would have performance implications as you would hook up new handlers to the promise for each access.

Torgeir
December 16, 2014

# re: Mixing $http Promises and $q Promises for cached Data

Hm..I don't think there will be any performance issues since Angular services are singletons and you will always be interacting with the same cached promise and calling the same then() method over and over again - passing in a regular callback for it to execute. The down side though is that every caller has to interact with it as a promise every time which means more code than just grabbing a resolved data value....

I go back and forth about caching promises vs the returned data value though.
It is definitely easier to grab a data value if you know your app is in a state where the initial promise is guaranteed to be resolved (e.g. after login in etc)
 

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