Diagnosing the AngularJS infdig Error: 10 $digest() Iterations Reached
The AngularJS infdig error means a watch never stabilizes across digest passes. Learn to read the watch list, isolate the offender, and apply the right fix — without masking it via digestTtl().
05 Mar 2026, 13:54 UTC

If your legacy AngularJS (1.x) page freezes and the console shows Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!, the framework is telling you something specific: after ten dirty-checking passes, at least one watched expression still produced a different value on every pass. AngularJS aborts rather than loop forever, and whatever was mid-update is left half-rendered. The fix is almost never "raise the limit" — it is finding the watch that never stabilizes. This guide walks through recognizing the condition, narrowing down the cause, and applying the right fix, for teams maintaining applications on AngularJS 1.x (which no longer receives security patches, so treat this as legacy maintenance guidance).
What the error actually means
AngularJS keeps the view in sync by running a digest cycle: it evaluates every registered watch expression, compares the result to the previous value, and repeats the whole sweep if anything changed, because one change can invalidate another watch. The cycle ends only when a full pass produces zero changes. The infdig error fires when ten consecutive passes still show changes — AngularJS assumes the model will never converge and stops.
The error message includes a list of the watch expressions that were still changing in the final passes. That list is your primary evidence; read it before changing any code.
Cause and diagnostic table
| Observation | Likely cause | Fix direction |
|---|---|---|
Trace names a function-based watch (e.g., fn: function (scope) {...}) | The watched function returns a new object or array identity each call, so equality checks never pass | Cache the computed value, or watch a stable primitive key instead |
| Error appears only after a user event or HTTP callback | A $watch listener mutates the very expression it watches (directly or via $broadcast/$apply chains) | Guard the mutation with an equality check; move the side effect out of the watcher |
Trace points at an ng-repeat over a filtered or sorted list | The filter allocates a new array each digest | Precompute the filtered list in the controller; store it on scope |
| None of the above; watch list looks benign | A third-party directive registers its own unstable watcher | Isolate directives; profile watcher counts per scope |
Ordered checks
- Read the watch list in the error message. The last entries are the expressions still oscillating. Copy them verbatim; they tell you whether you are dealing with a function watch, a filter, or a scope property.
- Reproduce in a minimal page. Strip the template down to the implicated binding plus its controller. A controller whose watched function returns
[]each call will triggerinfdigwithin seconds. If the minimal page does not reproduce, the trigger is an interaction (event, HTTP response) rather than the initial render. - Isolate by substitution. Temporarily replace a suspect function watch with
$watchCollection, or a suspect binding with a one-time binding (::expr). If the error disappears, you have confirmed which watch destabilizes the cycle. - Inspect listeners for side effects. For each remaining suspect, read the
$watchcallback and anything it calls. Look for assignments to scope variables that are themselves watched, and for$emit/$broadcastcalls whose handlers write back to the same scope.
A concrete reproduction and fix
The classic offender is a watch expression that allocates. In a controller or template:
// Controller: this watch NEVER stabilizes
$scope.$watch(function () {
return $scope.items.filter(function (i) { return i.active; });
}, function (newVal) {
$scope.activeCount = newVal.length;
});filter() returns a new array on every digest pass. AngularJS compares by reference for objects and arrays, so the watch fires every pass, the listener writes to scope, and another pass is scheduled — ten times, then infdig. Run this in any AngularJS 1.x page (no special permissions needed; it is ordinary controller code) and you will see the error in the browser console almost immediately.
The fix is to make the watched value stable. Either watch a primitive derived from the data:
$scope.$watch(function () {
return $scope.items.reduce(function (n, i) {
return n + (i.active ? 1 : 0);
}, 0);
}, function (count) {
$scope.activeCount = count;
});Or cache the computed collection and only replace it when the source actually changes — typically by computing it in the event handler that modifies items, not inside a watch. For shallow change detection on an existing collection, $watchCollection compares item-by-item rather than by identity, which tolerates a re-allocated array as long as its contents are the same references.
Fixes tied to findings
- Static or load-once data: use one-time bindings,
{{::item.name}}orng-repeat="x in ::list". The watch is deregistered once the expression yields a defined value, removing it from the digest entirely. Caution: if the expression is initiallyundefined, the watch stays alive until it becomes defined — but if it never does, the binding stays blank forever. Do not use::on data that legitimately starts undefined and is expected to render empty first. - Re-allocated collections: use
$watchCollection, or precompute and store the filtered/sorted list on scope when the underlying data changes, so the template binds to a stable reference. - Expensive watch functions: memoize. Return the cached result unless inputs changed, so the digest sees an identical value on repeat passes.
- Watcher side effects: move mutations into explicit event handlers (
ng-click, promise.then()callbacks). If a listener must write to a watched variable, guard it:if (newVal !== $scope.target) { $scope.target = newVal; }. A listener that converges in one pass does not loop.
What not to do
$rootScopeProvider.digestTtl() can raise the iteration limit above ten. Do not use it as a fix. An unstable watch still runs every pass; you have only changed how many wasted passes occur before the abort, and if the watch truly never converges the UI freezes for longer before failing. Treat the limit as a safety net, not a configuration knob.
Escalation criteria
Escalate beyond a local fix when:
- The loop persists after every watch expression in the trace has been stabilized — suspect a third-party directive registering its own watchers. Bisect by removing directives from the minimal reproduction.
- Watcher counts grow with scope nesting rather than with data. Walk the scope tree and sum
$$watchersper scope (a few lines in the browser console, run at a breakpoint) before and after your change. If the total is in the thousands for a single view, the structural fix is reducing watcher count — one-time bindings, fewerng-repeatfilters, or splitting the view — not chasing one expression. - The instability involves cross-scope communication (
$broadcaststorms). At that point map the event flow before editing; a local guard may just move the loop elsewhere.
Verifying the fix
Reload the page and confirm the console is clean, then exercise the view: edit the data that previously triggered the error and confirm bindings still update where they should — especially anywhere you introduced ::, which intentionally stops updating. Finally, compare watcher counts before and after using the scope-walking approach above. A real fix removes the oscillating watch or makes it converge; the count and the absence of infdig together confirm you found the hot spot rather than masked it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.