Should services expose their asynchronicity?

angularjs, angularjs-service

Solution

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).

Problem

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?

Original source