solving $rootScope:infdig Infinite $digest Loop

13,253

So I finally figured out my own problem and thought I would answer it for others in case someone else may find this info useful.

The crux of the fix had to do with 2 concepts: angular promises and angular watches. By being aware of and applying the 2 concepts together the fix was actually pretty simple.

Everything you put on $scope is 'watched' including functions. Every time something watched changes $scope.$apply() runs again to apply the changes. If a scope function (eg: $scope.isAdmin()) changes its return value from one 'apply' to the next it will trigger another 'apply', until things stabilize and the return value isn't changing.

But in my code I was returning user.then(...) which just returns a new promise (which kept the apply cycle going on forever since the return value kept changing).

In my isAdmin() function I needed to defer its return value until the user actually loaded (any other return value would be meaningless). So I changed the code to check if the user async call had resolved by checking $scope.user and if so returning a valid isAdmin value. If $scope.user was still not defined I would just return the promise I already created.

I changed the $scope.isAdmin() to be:

$scope.isAdmin = function() {
  if ($scope.user) {
    return $scope.user.isAdmin;
  }

  return user;
};

This has the same effect as the original code without triggering an infinite apply cycle. Specifically, if the $scope.user has not resolved we still return a promise as before, by returning the user var. Note however that the user var is the same promise not a new one created by then() so the apply cycle stabilizes.

And just for completeness here is the updated jsfiddle:

http://jsfiddle.net/eS5e5/2/

Share:
13,253
lostdorje
Author by

lostdorje

Updated on June 06, 2022

Comments

  • lostdorje
    lostdorje almost 2 years

    I get the basic idea of the infinite digest loop and how it happens, but I'm running into the problem. Here is a fiddle demonstrating my code and problem:

    http://jsfiddle.net/eS5e5/1/

    In the jsfiddle console you'll see the infinite digest loop.

    Basically I have to make decisions on data that may not have loaded yet so I need to wait for the promise to resolve using then(). I have a promise called user. There are two different places in the code where I call then() on user.

    1. Just after I define it. I need to set a scope variable based on it.
    2. In another scope method, $scope.isAdmin()

    For number 2, it might be asked why I just don't use $scope.user directly in the $scope.isAdmin() method. The problem is, it's possible for $scope.isAdmin() to be called before the async request for the user returns, in which case I need to 'block' before returning from $scope.isAdmin().

    My question is, what about $scope.isAdmin() is making angular think that a 'watched' variable has changed and that the digest cycle needs to run again?

    $scope.isAdmin() isn't actually changing anything.

    Here is the stripped down code:

    HTML:

    <body ng-controller='myController'>  
      <div ng-if='isAdmin()'>Hi! <strong>{{ user.username }}</strong> is an Admin!!!</div>
      <div ng-if='!isAdmin()'>Hi! <strong>{{ user.username }}</strong> is NOT an Admin!!!</div>
    </body>
    

    And the JS:

    angular.module('myApp', [])
      .factory('myService', function($q, $timeout) {
        return {        
          getUser: function() {
            var deferred = $q.defer();
    
            $timeout(function() {
              deferred.resolve({ username: 'me', isAdmin: true });
            }, 2000);
    
            return deferred.promise;
          }
        };
      })
      .controller('myController', function($scope, $q, myService) {      
        var getUserDeferred = $q.defer();
        var user = getUserDeferred.promise;
        user.then(function(user) {
          $scope.user = user;
          return user;
        });
    
        $scope.getUser = function() {
          return myService.getUser().then(function(user) {
            getUserDeferred.resolve(user);
          });
        };
    
        $scope.isAdmin = function() {
          return user.then(function(user) {
            return user.isAdmin;
          });
        };
    
        $scope.getUser();
      });
    
  • keithl8041
    keithl8041 about 8 years
    Thanks for answering your own question - this was the really relevant bit for me which made me understand my issue: But in my code I was [...] which just returns a new promise (which kept the apply cycle going on forever since the return value kept changing)