Infinite Digest Cycle in Angular Directive

By Dave Ceddia

Since this was a problem I ran into myself the other day, I thought I’d document the problem and solution here in case it helps someone.

The Problem

After adding some new code to your Angular app, you head to the browser and refresh. Nothing seems to be working. When you open up the browser console, you see messages like this:

Uncaught Error: 10 $digest() iterations reached. Aborting!

Something in your recent changes is causing something approaching an “infinite loop” in the Angular world.

This is the one that tripped me up recently:

Here’s a directive that calls a user-specified function when a button is clicked.

angular.directive('actionButton', function() {
  return {
    restrict: 'E',
    template: '<button ng-click="triggerAction()">click me</button>',
    scope: {
      model: '=',
      onAction: '='   // this is a mistake, should be '&'
    },
    link: function(scope, elem, attrs) {
      scope.triggerAction = function() {
        // Call the user's function when the button is pressed
        scope.onAction();
      }
    }
  };
});

And here’s the directive being used in a contrived example. Note that it calls updateStatus when the button is pressed…

<div ng-controller="DemoCtrl as ctrl">
  <action-button on-action="ctrl.updateStatus()"></action-button>
  <div>{{ ctrl.status }}</div>
</div>

Then inside updateStatus, we make a call to the server for some data.

angular.controller('DemoCtrl', function($http) {
  var ctrl = this;

  ctrl.updateStatus = function() {
    $http.get('/status').then(function(res) {
      ctrl.status = res.data;
    });
  };
});

What happens? Well, because the onAction attribute in the directive above is specified as an '=' binding, Angular will run and evaluate ctrl.updateStatus() and try to set the value of onAction. That’s not the right behavior – it should only execute updateStatus when the button is clicked.

To fix this, the line onAction: '=' should read onAction: '&'.

The problem is exacerbated by the fact that updateStatus calls a function that returns a promise. This causes a new digest cycle to be run every time the $http.get is called and again when the promise resolves – it’s a double whammy of terribleness.

Now is a good time to mention that you might (or, at least, I might) want to brush up on the different types of binding for isolate scopes in directives. The Angular docs on the Directive Definition Object cover those nicely.