Analysis and Solution of $digest Iteration Limit Error in AngularJS: The Pitfalls of Dynamic Sorting and ng-init

Dec 02, 2025 · Programming · 27 views · 7.8

Keywords: AngularJS | $digest cycle | ng-init | dirty checking | sorting filter

Abstract: This article provides an in-depth analysis of the common 'Error: 10 $digest() iterations reached. Aborting!' error in AngularJS applications. Through a specific case study, it explores the infinite $digest loop problem that occurs when using the orderBy filter in ng-repeat combined with ng-init modifying model data. The paper explains the principles of AngularJS's dirty checking mechanism, identifies how modifying model data during view rendering creates circular dependencies, and offers best practice solutions with data pre-calculation in controllers. It also discusses the limitations of the ng-init directive, providing practical guidance for developers to avoid similar errors.

Problem Phenomenon and Background

In AngularJS application development, developers sometimes encounter a confusing console error: Error: 10 $digest() iterations reached. Aborting!. This error typically indicates that AngularJS's dirty checking mechanism has entered an infinite loop. This article will analyze the root cause and solution of this problem through a specific case study.

Case Code Analysis

Consider the following AngularJS view template code:

<div ng-controller="AngularCtrl" ng-app>
  <div ng-repeat="user in users | orderBy:predicate:reverse | limitTo:10">
    <div ng-init="user.score=user.id+1">
        {{user.name}} and {{user.score}}
    </div>
  </div>
</div>

The corresponding controller code is:

function AngularCtrl($scope) {
    $scope.predicate = 'score';
    $scope.reverse = true;
    $scope.users = [{id: 1, name: 'John'}, {id: 2, name: 'Ken'}, {id: 3, name: 'smith'}, 
                    {id: 4, name: 'kevin'}, {id: 5, name: 'bob'}, {id: 6, name: 'Dev'}, 
                    {id: 7, name: 'Joe'}, {id: 8, name: 'kevin'}, {id: 9, name: 'John'}, 
                    {id: 10, name: 'Ken'}, {id: 11, name: 'John'}, {id: 1, name: 'John'}, 
                    {id: 2, name: 'Ken'}, {id: 3, name: 'smith'}, {id: 4, name: 'kevin'}, 
                    {id: 5, name: 'bob'}, {id: 6, name: 'Dev'}, {id: 7, name: 'Joe'}, 
                    {id: 8, name: 'kevin'}, {id: 9, name: 'John'}, {id: 10, name: 'Ken'}]
}

When $scope.reverse is set to true (descending order), running this code triggers the $digest iteration limit error. However, when $scope.reverse is set to false (ascending order), the error does not occur.

Error Mechanism Analysis

The core of the Error: 10 $digest() iterations reached. Aborting! error lies in AngularJS's dirty checking mechanism. AngularJS uses the $digest cycle to detect data changes and update views. When changes are detected, it re-runs all relevant watchers. If watcher execution causes new data changes, it triggers another round of $digest cycle.

In this case, the problem occurs in the line ng-init="user.score=user.id+1". During each ng-repeat iteration, ng-init modifies the user.score value. Since the view uses the orderBy:predicate:reverse filter to sort by score, changing the score value alters the sorting result, triggering a new rendering cycle.

Specifically, when reverse=true:

  1. Initial rendering begins
  2. ng-repeat iterates through user array
  3. ng-init sets score = id + 1 for each user
  4. Sort by score in descending order
  5. Sorting result changes user display order
  6. Sorting change triggers new $digest cycle
  7. New cycle executes ng-init again, modifying score values
  8. Infinite loop forms until 10-iteration limit is reached

When reverse=false, since the initial sorting (ascending by id corresponding to ascending score) matches the sorting after ng-init modifications, re-sorting is not triggered, thus avoiding the loop.

Solution

According to AngularJS best practices, the ng-init directive should only be used for toy examples or demonstration applications, not recommended for real production applications. The AngularJS official documentation clearly states: "Use ngInit directive in templates (for toy/example apps only, not recommended for real applications)".

The correct approach is to pre-calculate data in the controller:

function AngularCtrl($scope) {
    $scope.predicate = 'score';
    $scope.reverse = true;
    
    // Raw user data
    var rawUsers = [{id: 1, name: 'John'}, {id: 2, name: 'Ken'}, {id: 3, name: 'smith'}, 
                    {id: 4, name: 'kevin'}, {id: 5, name: 'bob'}, {id: 6, name: 'Dev'}, 
                    {id: 7, name: 'Joe'}, {id: 8, name: 'kevin'}, {id: 9, name: 'John'}, 
                    {id: 10, name: 'Ken'}, {id: 11, name: 'John'}, {id: 1, name: 'John'}, 
                    {id: 2, name: 'Ken'}, {id: 3, name: 'smith'}, {id: 4, name: 'kevin'}, 
                    {id: 5, name: 'bob'}, {id: 6, name: 'Dev'}, {id: 7, name: 'Joe'}, 
                    {id: 8, name: 'kevin'}, {id: 9, name: 'John'}, {id: 10, name: 'Ken'}];
    
    // Pre-calculate score in controller, avoid modifying model in view
    $scope.users = rawUsers.map(function(user) {
        return {
            id: user.id,
            name: user.name,
            score: user.id + 1  // Pre-calculated score value
        };
    });
}

The corresponding view template simplifies to:

<div ng-controller="AngularCtrl" ng-app>
  <div ng-repeat="user in users | orderBy:predicate:reverse | limitTo:10">
    {{user.name}} and {{user.score}}
  </div>
</div>

Deep Understanding of $digest Cycle

AngularJS's $digest cycle is the core mechanism of two-way data binding. It works through the following steps:

  1. Trigger $digest cycle (usually by user interaction, timers, or HTTP request completion)
  2. Execute all watcher functions, checking if values have changed
  3. If changes are detected, update DOM and mark dirty state
  4. Repeat steps 2-3 until no more changes (stable state) or 10-iteration limit is reached

The 10-iteration limit is a safety mechanism to prevent infinite loops from consuming browser resources. This error is easily triggered when code modifies watched data within watchers.

Another common error pattern is:

$scope.$watch('users', function(value) {
  $scope.users = [];  // Modifying watched data within watcher
});

This pattern immediately triggers the $digest iteration limit error because modifying users triggers the watcher, which again modifies users, creating an infinite loop.

Best Practices Summary

  1. Avoid modifying model data in views: Do not directly modify scope variables in ng-init, {{}} expressions, or DOM event handlers.
  2. Pre-calculate data in controllers: For data requiring calculation or transformation, complete it during controller initialization rather than during rendering.
  3. Use watchers cautiously: Ensure $watch callbacks do not modify watched data unless there are clear termination conditions.
  4. Understand filter impacts: When using orderBy, filter and other filters, be aware they may trigger re-rendering.
  5. Optimize ng-repeat with track by: For large lists, using track by helps AngularJS track items more efficiently, reducing unnecessary re-rendering.

By following these best practices, developers can avoid $digest iteration limit errors and build more stable, efficient AngularJS applications.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.