19

I'm writing a service that will retrieve data asynchronously ($http or $resource). I can hide the fact that it is asynchronous by returning an array that will initially be empty, but that will eventually get populated:

.factory('NewsfeedService1', ['$http', function($http) {
   var posts = [];
   var server_queried = false;
   return {
      posts: function() {
         if(!server_queried) {
            $http.get('json1.txt').success(
              function(data) {
                server_queried = true;
                angular.copy(data, posts);
            });
         }
         return posts;
      }
   };
}])
.controller('Ctrl1', ['$scope','NewsfeedService1',
function($scope, NewsfeedService1) {
    $scope.posts = NewsfeedService1.posts();
}])

Or I can expose the asynchronicity by returning a promise:

.factory('NewsfeedService2', ['$http', function($http) {
  var posts = [];
  var server_queried = false;
  var promise;
  return {
     posts_async: function() {
       if(!promise || !server_queried) {
         promise = $http.get('json2.txt').then(
           function(response) {
              server_queried = true;
              posts = response.data;
              return posts;
         });
       }
       return promise;
     }
  };
}])

.controller('Ctrl2', ['$scope','NewsfeedService2',
function($scope, NewsfeedService2) {
  NewsfeedService2.posts_async().then(
    function(posts) {
      $scope.posts = posts;
  });
  // or take advantage of the fact that $q promises are
  // recognized by Angular's templating engine:
  // (note that Peter and Pawel's AngularJS book recommends against this, p. 100)
  $scope.posts2 = NewsfeedService2.posts_async();
}]);

(Plunker - if someone wants to play around with the above two implementations.)

One potential advantage of exposing the asychronicity would be that I can deal with errors in the controller by adding an error handler to the then() method. However, I'll likely be catching and dealing with $http errors in an application-wide interceptor.

So, when should a service's asynchronicity be exposed?

Michelle Tilley
  • 157,729
  • 40
  • 374
  • 311
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • Great question I just realized I could do this to avoid having to watch variables on the service to update things in the scope directly (for cases where I don't need the singleton storage of data). Would be good to hear thoughts about when this might be a bad idea. – shaunhusain Jul 02 '13 at 03:49

3 Answers3

9

My guess is that you'll find people on both sides of this fence. Personally, I feel that you should always expose the asynchronicity of a library or function (or more correctly: I feel that you should never hide the asynchronicity of a library or function). The main reason is transparency; for example, will this work?

app.controller('MyController', function(NewsfeedService) {
  $scope.posts = NewsfeedService.posts();
  doSomethingWithPosts($scope.posts); // <-- will this work?
});

If you're using the first method (e.g. $resource), it won't, even though $scope.posts is technically an array. If doSomethingWithPosts has its own asynchronous operations, you could end up with a race condition. Instead, you have to use asynchronous code anyway:

app.controller('MyController', function(NewsfeedService) {
  $scope.posts = NewsfeedService.posts(function() {
    doSomethingWithPosts($scope.posts);
  });
});

(Of course, you can make the callback accept the posts as an argument, but I still think it's confusing and non-standard.)

Luckily, we have promises, and the very purpose of a promise is to represent the future value of an operation. Furthermore, since promises created with Angular's $q libraries can be bound to views, there's nothing wrong with this:

app.controller('MyController', function(NewsfeedService) {
  $scope.posts = NewsfeedService.posts();
  // $scope.posts is a promise, but when it resolves
  // the AngularJS view will work as intended.
});

[Update: you can no longer bind promises directly to the view; you must wait for the promise to be resolved and assign a scope property manually.]

As an aside, Restangular, a popular alternative to $resource, uses promises, and AngularJS' own $resource will be supporting them in 1.2 (they may already support them in the latest 1.1.x's).

Michelle Tilley
  • 157,729
  • 40
  • 374
  • 311
  • As another aside, I just noticed your comment: "(note that Peter and Pawel's AngularJS book recommends against this, p. 100)". Now I'm quite curious. :) – Michelle Tilley Jul 02 '13 at 03:57
  • 4
    I would add that empty array creates ambiguity when the server response is also an empty array. – Liviu T. Jul 02 '13 at 11:51
5

I would always go with async option since i don't like hiding the async nature of the underlying framework.

The sync version may look more clean while consuming it, but it inadvertently leads to bug where the developer does not realize that the call is async in nature and tries to access data after making a call.

SO is filled with questions where people make this mistake with $resource considering it sync in nature, and expecting a response. $resource also takes similar approach to option 1, where results are filled after the call is complete, but still $resource exposes a success and failure function.

AngularJS tries to hide the complexities of async calls if promises are returned, so binding directly to a promise feels like one is doing a sync call.

Chandermani
  • 42,589
  • 12
  • 85
  • 88
5

I say no, because it makes it harder to work with multiple services built this way. With promises, you can use $q.all() to make multiple request and respond when all of them complete, or you can chain operations together by passing the promise around.

There would be no intuitive way to do this for the synchronous style service.

Clark Pan
  • 6,027
  • 1
  • 22
  • 18