To then() or to success() in AngularJS

27 November 2014   17 comments   AngularJS, Javascript

Powered by Fusion×

By writing this I'm taking a risk of looking like an idiot who has failed to read the docs. So please be gentle.

AngularJS uses a promise module called $q. It originates from this beast of a project.

You use it like this for example:

angular.module('myapp')
.controller('MainCtrl', function($scope, $q) {
  $scope.name = 'Hello ';
  var wait = function() {
    var deferred = $q.defer();
    setTimeout(function() {
      // Reject 3 out of 10 times to simulate 
      // some business logic.
      if (Math.random() > 0.7) deferred.reject('hell');
      else deferred.resolve('world');
    }, 1000);
    return deferred.promise;
  };

  wait()
  .then(function(rest) {
    $scope.name += rest;
  })
  .catch(function(fallback) {
    $scope.name += fallback.toUpperCase() + '!!';
  });
});

Basically you construct a deferred object and return its promise. Then you can expect the .then and .catch to be called back if all goes well (or not).

There are other ways you can use it too but let's stick to the basics to drive home this point to come.

Then there's the $http module. It's where you do all your AJAX stuff and it's really powerful. However, it uses an abstraction of $q and because it is an abstraction it renames what it calls back. Instead of .then and .catch it's .success and .error and the arguments you get are different. Both expose a catch-all function called .finally. You can, if you want to, bypass this abstraction and do what the abstraction does yourself. So instead of:

$http.get('https://api.github.com/users/peterbe/gists')
.success(function(data) {
  $scope.gists = data;
})
.error(function(data, status) {
  console.error('Repos error', status, data);
})
.finally(function() {
  console.log("finally finished repos");
});

...you can do this yourself...:

$http.get('https://api.github.com/users/peterbe/gists')
.then(function(response) {
  $scope.gists = response.data;
})
.catch(function(response) {
  console.error('Gists error', response.status, response.data);
})
.finally(function() {
  console.log("finally finished gists");
});

It's like it's built specifically for doing HTTP stuff. The $q modules doesn't know that the response body, the HTTP status code and the HTTP headers are important.

However, there's a big caveat. You might not always know you're doing AJAX stuff. You might be using a service from somewhere and you don't care how it gets its data. You just want it to deliver some data. For example, suppose you have an AJAX request cached so that only the first time it needs to do an HTTP GET but all consecutive times you can use the stuff already in memory. E.g. Something like this:

angular.module('myapp')
.controller('MainCtrl', function($scope, $q, $http, $timeout) {

  $scope.name = 'Hello ';
  var getName = function() {
    var name = null;
    var deferred = $q.defer();
    if (name !== null) deferred.resolve(name);
    $http.get('https://api.github.com/users/peterbe')
    .success(function(data) {
      deferred.resolve(data.name);
    }).error(deferred.reject);
    return deferred.promise;
  };

  // Even though we're calling this 3 different times
  // you'll notice it only starts one AJAX request.
  $timeout(function() {
    getName().then(function(name) {
      $scope.name = "Hello " + name;
    });    
  }, 1000);

  $timeout(function() {
    getName().then(function(name) {
      $scope.name = "Hello " + name;
    });    
  }, 2000);

  $timeout(function() {
    getName().then(function(name) {
      $scope.name = "Hello " + name;
    });    
  }, 3000);
});

And with all the other promise frameworks laying around like jQuery's you will sooner or later forget if it's success() or then() or done() and your goldfish memory (like mine) will cause confusion and bugs.

So is there a way to make $http.<somemethod> return a $q like promise but with the benefit of the abstractions that the $http layer adds?

Here's one such possible solution maybe:

var app = angular.module('myapp');

app.factory('httpq', function($http, $q) {
  return {
    get: function() {
      var deferred = $q.defer();
      $http.get.apply(null, arguments)
      .success(deferred.resolve)
      .error(deferred.resolve);
      return deferred.promise;
    }
  }
});

app.controller('MainCtrl', function($scope, httpq) {

  httpq.get('https://api.github.com/users/peterbe/gists')
  .then(function(data) {
    $scope.gists = data;
  })
  .catch(function(data, status) {
    console.error('Gists error', response.status, response.data);
  })
  .finally(function() {
    console.log("finally finished gists");
  });
});

That way you get the benefit of a one same way for all things that get you data some way or another and you get the nice AJAXy signatures you like.

This is just a prototype and clearly it's not generic to work with any of the shortcut functions in $http like .post(), .put() etc. That can maybe be solved with a Proxy object or some other hack I haven't had time to think of yet.

So, what do you think? Am I splitting hairs or is this something attractive?

Follow @peterbe on Twitter

Comments

Evert Wiesenekker
Nice post! I am not an Angular Boss yet so I cannot comment on the code itself but thanks to you I am having a universal http construction which looks the same for all my http requests. For example I was using the http success/error/finally construction but proved not to work with Angular's Bootstrap typeahead. So I had to use the 'then' method, but I did not know how to use my original error/finally methods.
David Vdd
Thank you for this post.
I was trying to figure out how I could return a promise for a cached response in stead of a $http. I'm going to try and implement this. Since I'm only using get() it should work.

I don't understand why angularjs makes $http use a different promise callback.

By the way I think the last solution has a small typo:
  .error(deferred.resolve); => .error(deferred.reject);
Tomas Lycken
It seems that your main motivation is to avoid having one method return different types of promises (or data) depending on what happens, as in your example with an AJAX request the first time and loading from some cache on subsequent requests - is that correct?

If so, there's a much easier way to accomplish this: just chain then-calls on your promises, and reshape the data, until the client code can use the output consistently. For example, consider the following service method which takes an url and either gets the results from a server, or loads them from cache:

function loadData(url) {
    var deferred = $q.defer();
    if (isInCache(url)) {
        deferred.resolve(getFromCache(url));
    } else {
        $http.get(url).success(function(data) { deferred.resolve(data); });
    }
    return deferred.promise;
}

This gets even easier if your cache service is promise-aware:

function loadData(url) {
    if (isInCache(url)) {
        return getFromCache(url); // returns a then-able promise
    } else {
        return $http.get(url).success(function (data) { return data; }); // also a then-able promise
    }
}

My point is, if you build your services to have a consistent API, you don't need to hack around with proxies or wrappers around $http at all.
Peter Bengtsson
I love it!

I actually used this in another project since after I published this blog post :)
Andre Hogenkamp
I think that the when method from $q does exaclty what you want:
https://docs.angularjs.org/api/ng/service/$q (search for when(value);)
Isa
What I ended up doing is simulating the '.success' and '.error' promises of $http, and using an $http-like API everywhere for my service.

I'm not sure if this is bad, but I did this when returning a cached object:

            return {success: function(f){
                f(cachedObject.response);
            },

            error: function(f){

            }}

When the item is NOT cached, I simply return the original $http promise. So I can use my service like an $http promise everywhere...which is what I want to do because it is convenient.
Glenn Batuyong
I am working on an AngularJS Highcharts example based almost exactly on your blog post. Once the data is pulled from the service into the controller it's ready to use —however, the objects are inaccessible outside their scope. Please see my jsfiddle: https://jsfiddle.net/47ronin/y5c9cm5g/1/ —The challenge here is, Highcharts breaks if $scope.data and $scope.options are moved into an enclosing function. How can you "broadcast" the objects from within a function to an outside scope, as needed in my example. Thanks, and great blog post!
Garry Hicks
Hey Glenn,

Just curious if you found an answer to this issue.
Glenn Batuyong
Actually yes, scope was solved. Sorry for the late reply!
http://jsfiddle.net/rtyw81zw/2/
as referenced by Highcharts author:
https://github.com/gevgeny/ui-highcharts/issues/3#issuecomment-116047457
Daniel Sánchez
Everytime i used $http.get() / $http.post() to retrieve some json data i had to make something like this to be sure everything was json valid and error free from server,

app.controller('MainCtrl', function($scope, httpq) {
    $http.get('https://www.mysite.com/users/')
        .success(function(response){
              if (response != undefined && typeof response == "object") {
                     $scope.users = response.users;
              } else {
                     alert("MainCtrl -> Users: Result is not JSON type");
              }
        })
        .error(function(data) {
            alert("MainCtrl -> Users: Server Error");
        });

    $http.get('https://www.mysite.com/news/')
       .success(function(response){
              if (response != undefined && typeof response == "object") {
                     $scope.news= response.news;
              } else {
                     alert("MainCtrl -> News: Result is not JSON type");
              }
        })
        .error(function(data) {
            alert("MainCtrl -> News: Server Error");
        });
});


That's because entering the .success() does not guarantee its a json object, you can return from server a simple print "hello world"; and it will still enter .success()

Therefore, I was looking for something more practical, i ended on this page and after some reading i endend with this:


var app = angular.module('myapp');

app.factory('httpq', function($http, $q) {

  function createValidJsonRequest(httpRequest) {
      return {
          errorMessage: function (errorMessage) {
              var deferred = $q.defer();

              httpRequest
                  .success(function (response) {
                      if (response != undefined && typeof response == "object") {
                          deferred.resolve(response);
                      } else {
                          alert(errorMessage + ": Result is not JSON type");
                      }
                  })
                  .error(function(data) {
                      deferred.reject(data);
                      alert(errorMessage + ": Server Error");
                  });

              return deferred.promise;
          }
      };
  }

  return {
    getJSON: function() {
        return createValidJsonRequest($http.get.apply(null, arguments));
    },
    postJSON: function() {
        return createValidJsonRequest($http.post.apply(null, arguments));
    }
  }
});







app.controller('MainCtrl', function($scope, httpq) {
 
  httpq.getJSON('https://www.mysite.com/users/')
    .errorMessage("MainCtrl -> Users")
    .then(function(response) {
         $scope.users = response.users;
    });

  httpq.getJSON('https://www.mysite.com/news/')
    .errorMessage("MainCtrl -> News")
    .then(function(response) {
         $scope.news = response.news;
     })
     .catch(function(result){
          // do something in case of error
     });

  httpq.postJSON('https://www.mysite.com/news/', { ... })
    .errorMessage("MainCtrl -> addNews")
    .then(function(response) {
         $scope.news.push(response.new);
    });

});



Now i can use something like this knowing:
- It will be json valid.
- It will handle server errors.
- It will automatically display alerts showing a custom error message
- If it enters .then() everything went as planned.


Thanks Peter
Jr
hmmm probably I'm in need of some javascript knowledge because, how can this piece of code not fire three Ajax requests?

var getName = function() {
    var name = null;
    var deferred = $q.defer();
    if (name !== null) deferred.resolve(name);
    $http.get('https://api.github.com/users/peterbe')
    .success(function(data) {
      deferred.resolve(data.name);
    }).error(deferred.reject);
    return deferred.promise;
  };

When called three times with the $timeout, the name variable is local to the function and it gets set every call to null. So that would mean that this condition is always false?
if (name !== null) deferred.resolve(name);

Meaning no caching is happening here, I know it's not the main subject of this article but still.
Correct me if I'm wrong.
Thanks J
Peter Bengtsson
If you make that 'var name' global instead and assign to it inside the success callback you'll be fine.
But also, you'll need to put the $http.get in an else block.
steven kauyedauty
I am curious how to use .then with $q or if it is even necessary. I grew accustomed to .success and .error but those have been depreciated.
Ajinkya
thanks i didnt knew angular $http has success and error callbacks
Carl Armbruster
I tried to do something like this about 6 months ago but didn't really have a good handle on promises and on using promises in services. We had other work to do so we just make our required call synchronouse with a quick class I created. Ugly, yes. Worked, yes. Fast forward and I finally have a chance to revisit the ugly code I wrote and did a quick search on promises and found this. After quite a bit more experience with promises your simple example above explains exactly the piece I was missing with defferred promises.

Fixing my ugly code now - thanks!
Mark N Hopgood
Great post! I 'extended' my CRM code with a variant of this.
Many thanks for the idea.
Anonymous
Hi,
note .success and .error "methods" wa removed from the release 1.6 of Angular.js
Thank you for posting a comment

Your email will never ever be published


Related posts

Previous:
A "perma search" in AngularJS 18 November 2014
Next:
vipsthumbnail 08 December 2014
Related by Keyword:
Chainable catches in a JavaScript promise 05 November 2015
AngularJS $q notify and resolve as a local GET proxy 18 April 2015
Related by Text:
AngularJS $q notify and resolve as a local GET proxy 18 April 2015
Chainable catches in a JavaScript promise 05 November 2015
What stumped me about AngularJS 12 May 2013
Integrate BrowserID in a Tornado web app 22 November 2011
QUnit testing my jQuery Mobile site in full swing 17 March 2011