When $scope.$apply()
is used incorrectly in AngularJS, it can trigger an infinite digest loop, leading to performance issues or even crashing the browser. This usually happens when $apply()
is repeatedly called inside a function that is already inside a digest cycle.
1. Understanding $scope.$apply()
and the Digest Cycle
AngularJS has a digest cycle that checks for changes in $scope
variables and updates the DOM accordingly. When $apply()
is called, it manually triggers a digest cycle, which can cause problems if not used properly.
AngularJS already runs a digest cycle in these cases:
Inside AngularJS directives (e.g., ng-click
, ng-model
)
Inside $http
, $timeout
, $interval
, $watch
Calling $apply()
inside these can cause an infinite loop.
2. Common Causes of Infinite Digest Loop
Issue | Explanation |
---|---|
Calling $scope.$apply() inside $watch | $watch is called every digest cycle, so calling $apply() inside it creates an endless loop. |
Using $scope.$apply() inside event listeners repeatedly | If an event fires rapidly (e.g., mousemove , scroll ), $apply() continuously triggers new digest cycles. |
Calling $apply() inside Angular’s async services | $http , $timeout , and $interval already trigger digest cycles, so calling $apply() is unnecessary. |
Forgetting to wrap $apply() inside $digest check | Calling $apply() inside a running digest cycle throws an error. |
3. Fixing Infinite Digest Loop Issues in $scope.$apply()
Fix 1: Use $scope.$applyAsync()
Instead
If $apply()
is triggering an infinite loop, use $applyAsync()
instead, which schedules an update after the current digest cycle completes.
Incorrect (Causes Infinite Loop in $watch
)
$scope.$watch('counter', function(newVal, oldVal) {
$scope.$apply(); // Infinite loop because $watch triggers every digest cycle
});
Correct (Use $applyAsync()
to avoid looping)
$scope.$watch('counter', function(newVal, oldVal) {
$scope.$applyAsync(); // Runs in the next digest cycle, preventing infinite loop
});
Fix 2: Wrap $apply()
in $scope.$$phase
Check
To prevent calling $apply()
inside an active digest cycle, use $scope.$$phase
to check if Angular is already running a digest cycle.
Incorrect ($apply()
runs inside Angular event)
$scope.increment = function() {
$scope.$apply(() => { $scope.counter++; }); // Causes an infinite loop
};
Correct (Check $scope.$$phase
before calling $apply()
)
$scope.increment = function() {
if (!$scope.$$phase) {
$scope.$apply(() => { $scope.counter++; }); // Only applies changes if digest is not running
}
};
$scope.$$phase
prevents calling $apply()
when a digest cycle is already in progress.
Fix 3: Remove $apply()
from $http
, $timeout
, or $interval
AngularJS automatically triggers a digest cycle when using $http
, $timeout
, or $interval
, so calling $apply()
manually is unnecessary.
Incorrect (Unnecessary $apply()
in $http
)
$http.get("/api/data").then(function(response) {
$scope.$apply(() => { $scope.data = response.data; }); // Unnecessary and may cause looping
});
Correct (Remove $apply()
)
$http.get("/api/data").then(function(response) {
$scope.data = response.data; // No need for `$apply()`
});
The same applies to $timeout
and $interval
.
Fix 4: Use $timeout
Instead of $apply()
If you’re using $apply()
in an event that’s not inside AngularJS (e.g., window.addEventListener
), use $timeout
instead.
Incorrect (Using $apply()
inside resize
event)
window.addEventListener("resize", function() {
$scope.$apply(() => { $scope.width = window.innerWidth; }); // Infinite loop risk
});
Correct (Use $timeout
)
window.addEventListener("resize", function() {
$timeout(() => { $scope.width = window.innerWidth; }); // Works without infinite loops
});
$timeout
schedules a digest cycle only when necessary, avoiding loops.
Fix 5: Use $scope.$digest()
Instead of $apply()
If only a small part of the scope needs updating, $digest()
is a safer alternative to $apply()
.
Incorrect ($apply()
triggers a full digest cycle)
$scope.updateValue = function() {
$scope.$apply(() => { $scope.value = "New Value"; }); // ❌ Causes unnecessary full digest cycle
};
Correct ($digest()
updates only the current scope)
$scope.updateValue = function() {
$scope.value = "New Value";
$scope.$digest(); // Updates only this scope, preventing unnecessary digest loops
};
Use $digest()
instead of $apply()
when you don’t need a full digest cycle.
4. Debugging Infinite Digest Loops in Chrome DevTools
If your AngularJS app is freezing or lagging, an infinite digest loop may be the cause. You can debug it in Chrome DevTools.
Steps to Debug Infinite Digest Loop
- Open DevTools (
F12
orCtrl + Shift + I
) → Go to Console. - Run:
angular.element(document.body).scope()
If you see too many scope updates, it indicates excessive digest cycles. - Check how many digest cycles are running:
angular.element(document.body).scope().$$watchersCount
If the watcher count is too high, an infinite loop might be happening. - Temporarily disable
$apply()
and see if performance improves.
5. Final Checklist
Fix | Solution |
---|---|
Use $applyAsync() instead of $apply() | Prevents infinite loops by delaying the digest cycle. |
Check $scope.$$phase before calling $apply() | Ensures $apply() only runs when needed. |
Avoid $apply() in $http , $timeout , or $interval | These services already trigger a digest cycle. |
Use $timeout() instead of $apply() in event listeners | Allows Angular to manage the digest cycle correctly. |
Use $digest() instead of $apply() when possible | Limits the digest cycle to the current scope. |
Use Chrome DevTools to check $scope.$$watchersCount | Helps detect excessive digest cycles. |