Debugging Angular's digest cycle: How to find the cause of an infinite loop?

Domi picture Domi · May 21, 2014 · Viewed 7.3k times · Source

I currently am working on some code with lazy-loaded content + controllers. My code basically works like this fiddle. However, for some reason, my version does not work, and instead I get an infinite digest cycle whenever angular tries to update it's view.

The issue disappears when I remove ng-include from this simple repeat statement:

<div class="container" ng-repeat="pageName in pageNames">
  <div ng-include="pageName"></div>
</div>

Weirdest part: The exact same error occurs even if pageNames is never assigned to the scope. Both scopes (the scope of outer and inner controller - I have each one) can be completely empty (I checked with Batarang - I only have two empty scopes), and I still get the error.

My code is a bit too involved, with too many other dependencies, so posting it here makes no sense. It's purest version is the fiddle above. I cannot find a difference in the logic between the two. When I inspect my scopes with Batarang, I don't see anything suspicious either:

  • I am not using functions in my scopes
  • I did not make use of $watch
  • I did not make use of ng-model

I conclude that I am not explicitly changing anything, so it must be something under the hood of angular.

Can I somehow get Angular or Batarang to tell me which scope variables have changed after a digest iteration, so I can identify the culprit causing the infinite loop?

UPDATE:

I finally figured out that history.pushState messes everything up. I am now looking into alternatives, such as the $location service. Nevertheless, I would still like to know how to debug this kind of issue in general. Any hints?

Answer

Chad Robinson picture Chad Robinson · Oct 4, 2014

There's no direct way to get a list of scope variables that changed in each digest, but it's really simple to modify Angular's source to do that (temporarily) if you're in an emergency-debugging situation. The $digest code is here:

angular.js > src > ng > rootScope.js

If you look at this method you'll see references to a watchLog array. AngularJS uses this to report useful error messages when it detects looping like you're getting. You could easily add a few console.log items here, or maintain your own array that you could log later or inspect with your debugger. You can also set breakpoints here to see what is going on.

It's not clear to me why history.pushState() would have anything to do with this because your original question doesn't talk about the use of $location, history, or related functions. However, note that it is a VERY common problem with ngInclude to create infinite loops when paths aren't what you expect. A huge number of server-side (e.g. NodeJS) projects are configured to return the home/index page when a resource isn't found because they assume there's front-end routing going on. That's actually a huge problem because you end up embedding your home page in your home page, which then embeds another copy, etc. I see this kind of problem posted about once a week here on S/O.

A simple test for this is to alter your server to return an empty 404 for any resource not found. I always recommend this whenever somebody mentions "infinite loop" and any kind of asset loading operation: ui-router, ngRoute, ngInclude, etc.

This is particularly common when using relative URLs in paths.