Do I need $rootScope.$apply( ) in a service if I'm returning a promise?

4,785 views
Skip to first unread message

Davis Ford

unread,
Apr 16, 2013, 1:29:28 PM4/16/13
to ang...@googlegroups.com
I was always under the impression that if you were using $q in a service, and returning the promise, you did not need to wrap in scope.$apply -- but then again, the docs for $q show the async setTimeout is wrapped in scope.$apply => http://docs.angularjs.org/api/ng.$q

That begs a second question, the example there just assumes "scope is injected somehow"

  1. // for the purpose of this example let's assume that variables `$q` and `scope` are
  2. // available in the current lexical scope (they could have been injected or passed in).

In a service, there is no scope, per se.  You can inject the $rootScope.  So, here's my conundrum / question.  Let's say I have a 3rd party async api that looks like this:

foo.doSomething( function success (data) { /* do something with data */ }, function error (err) { /* do something with error */ } );

I want to wrap this in a service, so which way is correct?

app.factory('myservice', ['$q', function ($q) {

  return {
     get: function () {
       var deferred = $q.defer();

       foo.doSomething(function (data) { 
          promise.resolve(data);
       }, function (err) { 
          promise.reject(err);
       });

      return deferred.promise;
    }
 };

}]); 


OR

app.factory('myservice', ['$q', '$rootScope', function ($q, $rootScope) {

  return {
     get: function () {
        var deferred = $q.defer();

        $rootScope.$apply(function () {
           
          foo.doSomething(function (data) {
            promise.resolve(data);
          }, function (err) {
            promise.reject(err);
          });
        });

       return deferred.promise;
    }
  };
}]);

Josh David Miller

unread,
Apr 16, 2013, 1:43:13 PM4/16/13
to angular
Hello!

In this case, you need to wrap the promise call it in a call to $apply because the call itself is done outside of AngularJS. Here's a Plunker: http://plnkr.co/edit/eBCmmDG7tXECcenw8Zcx.

Also note, though, that the entire call shouldn't be in a wrap to $apply - only the part outside of AngularJS. i.e. the "promise.resolve" and "promise.reject" calls but not their wrapping function as the wrapping function is already in a $digest cycle. It is the async nature of the event that needs the call to $apply.

Josh



--
You received this message because you are subscribed to the Google Groups "AngularJS" group.
To unsubscribe from this group and stop receiving emails from it, send an email to angular+u...@googlegroups.com.
To post to this group, send email to ang...@googlegroups.com.
Visit this group at http://groups.google.com/group/angular?hl=en-US.
For more options, visit https://groups.google.com/groups/opt_out.
 
 

Davis Ford

unread,
Apr 16, 2013, 2:02:06 PM4/16/13
to ang...@googlegroups.com
Thanks Josh,

I am getting a '$digest already in progress' error when I do wrap

$rootScope.$apply(function ( ) { promise.resolve(data); });

This ends up going into the $exceptionHandler, so the data doesn't reach the controller.

Josh David Miller

unread,
Apr 16, 2013, 3:36:10 PM4/16/13
to angular
Can you post a Plunker? It sounds like the call isn't actually asynchronous.


Davis Ford

unread,
Apr 16, 2013, 3:59:40 PM4/16/13
to ang...@googlegroups.com
It is async -- unfortunately, I can't post a working plunk as this 3rd party lib won't run in plnkr.co -- requires a native counterpart, but the pattern is identical to what you've got.  I'll keep digging, I guess.  I understand your example, and I understand what you are saying, but something still seems awry.  It seems like angular's nextTick( ) hasn't completed when the callback is executed -- that seems to be the issue I am having.  

Ironically, the target for this is an embedded device with much slower resources, and the reason I'm wrestling with this is that originally I did not have the wrapper in place:  $rootScope.$apply(function () { promise.resolve(data); }); and the nextTick( ) always finished before the callback occurred, and so my controller wasn't getting the view updated b/c the $digest cycle was complete.

So, now I add the wrapper, and when I run in the dev environment (i.e. laptop - not target hardware), the $digest cycle doesn't seem to complete before the callback fires, and throws the 'digest already in progress' error.  It seems I can't win either way, and getting an online demonstration of this will be a real hurdle for a number of reasons.

Josh David Miller

unread,
Apr 16, 2013, 4:19:37 PM4/16/13
to angular
Hmm... the issue must be within the native counterpart. Perhaps part of it is blocking. The only suggestion I have is to add a bunch of console.log statements throughout the code to see if you can establish the current execution order. Unfortunately, without code I don't have a better suggestion. :-(

Out of curiosity, what is the native component?

Good luck!

Davis Ford

unread,
Apr 16, 2013, 4:48:52 PM4/16/13
to ang...@googlegroups.com
I did solve it -- thanks to you suggestion that perhaps it wasn't executing async...well the API is async (i.e. takes a callback), but it is implemented by taking a function as a parameter, and then down in the guts somewhere just doing a fn.call(null, params). 

It is more complicated -- there is an emulator with stubbed out native code, and then there's the real platform -- where it does execute asynchronously b/c it crosses a native bridge.  So in the emulator, it behaves synchronously, and on the target hardware it behaves asynchronously.

I solved it (complete hack) by wrapping it with 

setTimeout(function () {
  callSometimesAsyncFn( cb );
}, 0);

with a timeout value of zero.  This forces a nextTick();

Adam Tegen

unread,
Jul 2, 2015, 11:23:56 AM7/2/15
to ang...@googlegroups.com
The solution I've gone with, for this exact same scenario is:

var inScope = true;
callSometimesAsyncFn(function() {
  if (inScope) {
    cb();
  }
  else {
    $rootScope.$apply(cb);
  }
});
inScope = false;

That way, if it callback is called synchronously, its not wrapped in a scope.$apply, but if its async, it is.

Adam

Alain Chautard

unread,
Jul 2, 2015, 12:41:24 PM7/2/15
to ang...@googlegroups.com
Instead of usine setTimeout(), you could use the $timeout service from angular. It would look like this:

$timeout(function () {
  callSometimesAsyncFn( cb );
});
Reply all
Reply to author
Forward
0 new messages