1

I have 2 plunkr's ready to demonstrate what I'm confused about. I am seeking a clear explanation as to precisely why the first plunkr fails, yet the second works.

In this first plunkr, I simulate calling into an external library to do some kind of networked authentication. The problem is, when there are 2 promises in the chain from the original controller level method, the function passed to the first promise for execution upon resolve never fires and neither do any other promises further down the chain despite resolving each promise.

http://plnkr.co/edit/6uKnVvEI3bJvfmaUoWN0

However, when I change the calls to use a $timeout, regardless of whether it is used to simulate a delay, or just to wrap a deferred.resolve that comes back from an actual external operation (like calling a REST API), everything works as expected. In this second plunkr, you can see the login functionality working just fine once both deferred.resolve calls have been modified to be wrapped in a $timeout call.

Additionally, I included a test case that someone else suggested as the problem where resolving a promise before returning it would fail. This is clearly not the case as this second plunkr works just fine when doing precisely that. Note that this alternate method does not use a $timeout but still works just fine. This indicates to me that there is something special about the relationship between the two services (testApi, authService) when both return promise objects resulting in a chain of nested promises that have to resolve upwards.

http://plnkr.co/edit/xp8NeZKWDep6cPys5gJu?p=preview

Is anyone able to explain to me why these promises fail in one instance but work in another when they are either not nested, or if nested wrapped in a $timeout?

My hunch is something related to the digest cycle, but for the life of me I cannot understand why the digest cycle would affect services running essentially independent of a controller. They are not properties on the $scope that need to resolve before the controller loads, but functions wrapping service calls that return a promise.

Ross
  • 472
  • 1
  • 4
  • 11

1 Answers1

3

Your hunch is right. When you use setTimeout, it triggers an event Angular knows nothing about; that's not the case if you use $timeout instead. So, for your first Plunker script to work, you need to start a digest cycle manually by calling $rootScope.$apply():

angular.module('testApi', []).factory('testApi', function($q, $rootScope) {
...    
    setTimeout(function() { 
        $rootScope.$apply(function() { 
            deferred.resolve({user: { id: '1', name: 'bozo'}}); 
        });
    }, 1000);

Plunker here.

You won't need to do any of the above if you stick with $timeout, and I suggest that you do that.

This SO question has more info on why promises callbacks are only called upon entering a $digest cycle.

Community
  • 1
  • 1
Michael Benford
  • 14,044
  • 3
  • 60
  • 60
  • You seem smart and as such I have a follow up question. Say a service that is used independently of a controller (prefetch): calling $timeout there does not seem to force a digest cycle specifically in the case where taking a bunch of external compatible promises from a 3rd party library and using q.all to process as an array. The .then function of the promise returned from q.all is not called until the next digest cycle, and I can't force it either. Also, injecting $rootScope and using $apply does not seem safe, as quite often you get the digest in progress error. Any suggestions/tips? – Ross Sep 17 '13 at 01:39
  • For future reference: I solved it by passing in a function to the .then method of the 3rd party promises that calls $timeout.. `promises.push(my3rdPartyLib.Object.method(argument).then(function(success) { $timeout(function() {});}));` AND LATER `$q.all(promises).then(function(success) { timeout(function() { deferred.resolve(success); }); });` – Ross Sep 17 '13 at 02:03
  • There's no harm in using `$rootScope` as long as you know what you're doing. In fact, the `$timeout` service internally calls `$rootScope.$apply()` if you pass true as its third argument. – Michael Benford Sep 17 '13 at 02:46
  • Yes the trick for me was realising that the digest cycle wasn't happening because the dependency on the array of promises from a 3rd party library needed to call a digest cycle themselves, failure to do so results in the whole chain of promises from the 3rd party library down to the service consuming the service that wraps the 3rd party library waiting to resolve until a digest cycle happened independently. The only way I could get this to happen was to call $timeout in the returned promises from the 3rd party library (which uses when.js as opposed to Q/q-lite). – Ross Sep 17 '13 at 05:27