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:
- Initial rendering begins
ng-repeatiterates through user arrayng-initsetsscore = id + 1for each user- Sort by
scorein descending order - Sorting result changes user display order
- Sorting change triggers new
$digestcycle - New cycle executes
ng-initagain, modifyingscorevalues - 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:
- Trigger
$digestcycle (usually by user interaction, timers, or HTTP request completion) - Execute all watcher functions, checking if values have changed
- If changes are detected, update DOM and mark dirty state
- 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
- Avoid modifying model data in views: Do not directly modify scope variables in
ng-init,{{}}expressions, or DOM event handlers. - Pre-calculate data in controllers: For data requiring calculation or transformation, complete it during controller initialization rather than during rendering.
- Use watchers cautiously: Ensure
$watchcallbacks do not modify watched data unless there are clear termination conditions. - Understand filter impacts: When using
orderBy,filterand other filters, be aware they may trigger re-rendering. - Optimize ng-repeat with track by: For large lists, using
track byhelps 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.