Skip to content

Commit 848280c

Browse files
docs(error/$rootScope/inprog): improve understanding and diagnosis of the error
See angular#5549
1 parent aa26856 commit 848280c

File tree

1 file changed

+295
-34
lines changed

1 file changed

+295
-34
lines changed

docs/content/error/$rootScope/inprog.ngdoc

+295-34
Original file line numberDiff line numberDiff line change
@@ -3,72 +3,333 @@
33
@fullName Action Already In Progress
44
@description
55

6-
At any point in time there can be only one `$digest` or $apply operation in progress.
7-
The stack trace of this error allows you to trace the origin of the currently executing $apply or $digest call.
6+
At any point in time there can be only one `$digest` or `$apply` operation in progress. This is to
7+
prevent very hard to detect bugs from entering your application. The stack trace of this error
8+
allows you to trace the origin of the currently executing `$apply` or `$digest` call, which caused
9+
the error.
810

9-
`$digest` or `$apply` are processing operational states of the Scope - data-structure in Angular that provides context for models and enables model mutation observation.
11+
## Background
1012

11-
Trying to reenter a `$digest` or `$apply` while one of them is already in progress is typically a sign of programming error that needs to be fixed.
13+
Angular uses a dirty-checking digest mechanism to monitor and update values of the scope during
14+
the processing of your application. The digest works by checking all the values that are being
15+
watched against their previous value and running any watch handlers that have been defined for those
16+
values that have changed.
17+
18+
This digest mechanism is triggered by calling `$digest` on a scope object. Normally you do not need
19+
to trigger a digest manually, because every external action that can trigger changes in your
20+
application, such as mouse events, timeouts or server responses, wrap the Angular application code
21+
in a block of code that will run `$digest` when the code completes.
22+
23+
You wrap Angular code in a block that will be followed by a `$digest` by calling `$apply` on a scope
24+
object. So, in pseudo-code, the process looks like this:
25+
26+
```
27+
element.on('mouseup', function() {
28+
scope.$apply(function() {
29+
$scope.doStuff();
30+
});
31+
});
32+
```
33+
34+
where apply looks something like:
35+
36+
```
37+
$apply = function(fn) {
38+
try {
39+
fn();
40+
} finally() {
41+
$digest();
42+
}
43+
}
44+
```
45+
46+
## Digest Phases
47+
48+
Angular keeps track of what phase of processing we are in, the relevant ones being `$apply` and
49+
`$digest`. Trying to reenter a `$digest` or `$apply` while one of them is already in progress is
50+
typically a sign of programming error that needs to be fixed. So Angular will throw this error when
51+
that occurs.
52+
53+
In most situations it should be well defined whether a piece of code will be run inside an `$apply`,
54+
in which case you should not be calling `$apply` or `$digest`, or it will be run outside, in which
55+
case you should wrap any code that will be interacting with Angular scope or services, in a call to
56+
`$apply`.
57+
58+
As an example, all Controller code should expect to be run within Angular, so it should have no need
59+
to call `$apply` or `$digest`. Conversely, code that is being trigger directly as a call back to
60+
some external event, from the DOM or 3rd party library, should expect that it is never called from
61+
within Angular, and so any Angular application code that it calls should first be wrapped in a call
62+
to $apply.
63+
64+
## Common Causes
65+
66+
Apart from simply incorrect calls to `$apply` or `$digest` there are some cases when you may get
67+
this error through no fault of your own.
68+
69+
### Inconsistent API (Sync/Async)
1270

1371
This error is often seen when interacting with an API that is sometimes sync and sometimes async.
1472

15-
For example:
73+
For example, imagine a 3rd party library that has a method which will retrieve data for us. Since it
74+
may be making an asynchronous call to a server, it accepts a callback function, which will be called
75+
when the data arrives.
1676

1777
```
18-
function MyController() {
78+
function MyController($scope, thirdPartyComponent) {
1979
thirdPartyComponent.getData(function(someData) {
20-
scope.$apply(function() {
21-
scope.someData = someData;
80+
$scope.$apply(function() {
81+
$scope.someData = someData;
2282
});
2383
});
2484
}
2585
```
2686

27-
The controller constructor is always instantiated from within an $apply cycle, so if the third-party component called our callback synchronously, we'd be trying to enter the $apply again.
87+
We expect that our callback will be call asynchronously, and so from outside Angular. Therefore, we
88+
correctly wrap our application code that interacts with Angular inside a call to `$apply`.
2889

29-
To resolve this type of issue, either fix the api to be always synchronous or asynchronous or wrap the call to the api with setTimeout call to make it always asynchronous.
90+
The problem comes if `getData()` decides to call the callback handler synchronously; perhaps it has
91+
the data already cached in memory and so it immediately calls the callback to return the data,
92+
synchronously.
3093

94+
Since, the `MyController` constructor is always instantiated from within an `$apply` call, our
95+
handler is trying to enter a new `$apply` block from within one.
3196

32-
Other situation that leads to this error is when you are trying to reuse a function to by using it as a callback for code that is called by various apis inside and outside of $apply.
97+
This is really a poor design choice on the part of the 3rd party library.
3398

34-
For example:
99+
To resolve this type of issue, either fix the api to be always synchronous or asynchronous or force
100+
your callback handler to always run asynchronously by using the `$timeout` service.
35101

36102
```
37-
myApp.directive('myDirective', function() {
103+
function MyController($scope, thirdPartyComponent) {
104+
thirdPartyComponent.getData(function(someData) {
105+
$timeout(function() {
106+
$scope.someData = someData;
107+
}, 0);
108+
});
109+
}
110+
```
111+
112+
Here we have used `$timeout` to schedule the changes to the scope in a future call stack.
113+
By providing a timeout period of 0ms, this will occur as soon as possible and `$timeout` will ensure
114+
that the code will be called in a single `$apply` block.
115+
116+
### Triggering Events Programmatically
117+
118+
The other situation that often leads to this error is when you trigger code (such as a DOM event)
119+
programmatically (from within Angular), which is normally called by an external trigger.
120+
121+
For example, consider a directive that will set focus on an input control when a value in the scope
122+
is true:
123+
124+
```
125+
myApp.directive('setFocusIf', function() {
38126
return {
39-
link: function($scope, $element) {
40-
function doSomeWork() {
41-
$scope.$apply(function() {
42-
// do work here, and update the model
43-
};
44-
}
45-
46-
$element.on('click', doSomeWork);
47-
doSomeWork(); // << this will throw an exception because templates are compiled within $apply
127+
link: function($scope, $element, $attr) {
128+
$scope.$watch($attr.setFocusIf, function(value) {
129+
if ( value ) { $element[0].focus(); }
130+
});
48131
}
49-
}
132+
};
50133
});
134+
```
51135

136+
If we applied this directive to an input which also used the `ngFocus` directive to trigger some
137+
work when the element receives focus we will have a problem:
138+
139+
```
140+
<input set-focus-if="hasFocus" ng-focus="msg='has focus'">
141+
<button ng-click="hasFocus = true">Focus</button>
52142
```
53143

54-
The fix for the example above looks like this:
144+
In this setup, there are two ways to trigger ngFocus. First from a user interaction:
145+
146+
* Click on the input control
147+
* The input control gets focus
148+
* The `ngFocus` directive is triggered, setting `$scope.msg='has focus'` from within a new call to
149+
`$apply()`
150+
151+
Second programmatically:
152+
153+
* Click the button
154+
* The `ngClick` directive sets the value of `$scope.hasFocus` to true inside a call to `$apply`
155+
* The `$digest` runs, which triggers the watch inside the `setFocusIf` directive
156+
* The watch's handle runs, which gives the focus to the input
157+
* The `ngFocus` directive is triggered, setting `$scope.msg='has focus'` from within a new call to
158+
`$apply()`
159+
160+
In this second scenario, we are already inside a `$digest` when the ngFocus directive makes another
161+
call to `$apply()`, causing this error to be thrown.
162+
163+
It is possible to workaround this problem by moving the call to set the focus outside of the digest,
164+
by using the `$scope.$$postDigest(fn)`:
165+
55166
```
56-
myApp.directive('myDirective', function() {
167+
myApp.directive('setFocusIf', function() {
57168
return {
58-
link: function($scope, $element) {
59-
function doSomeWork() {
60-
// do work here, and update the model
61-
}
62-
63-
$element.on('click', function() {
64-
$scope.$apply(doSomeWork); // <<< the $apply call was moved to the callsite that doesn't execute in $apply call already
169+
link: function($scope, $element, $attr) {
170+
$scope.$watch($attr.setFocusIf, function(value) {
171+
if ( value ) {
172+
$scope.$$postDigest(function() {
173+
// We must reevaluate the value in case it was changed by a subsequent
174+
// watch handler in the digest.
175+
if ( $scope.$eval($attr.setFocusIf) ) {
176+
$element[0].focus();
177+
}
178+
});
179+
}
65180
});
181+
}
182+
};
183+
});
184+
```
185+
186+
187+
or `$timeOut(fn, 0, false)`, where the `false` value tells Angular not to wrap this `fn` in a `$apply`
188+
block:
66189

67-
doSomeWork();
190+
```
191+
myApp.directive('setFocusIf', function($timeout) {
192+
return {
193+
link: function($scope, $element, $attr) {
194+
$scope.$watch($attr.setFocusIf, function(value) {
195+
if ( value ) {
196+
$timeout(function() {
197+
// We must reevaluate the value in case it was changed by a subsequent
198+
// watch handler in the digest.
199+
if ( $scope.$eval($attr.setFocusIf) ) {
200+
$element[0].focus();
201+
}
202+
}, 0, false);
203+
}
204+
});
68205
}
69206
}
70207
});
208+
```
209+
210+
## Diagnosing This Error
211+
212+
When you get this error it can be rather daunting to diagnose the cause of the issue. The best
213+
course of action is to investigate the stack trace from the error. You need to look for places
214+
where `$apply` or `$digest` have been called and find the context in which this occurred.
215+
216+
There should be two calls:
217+
218+
* The first call is the good `$apply`/`$digest` and would normally be triggered by some event near
219+
the top of the call stack.
220+
221+
* The second call is the bad `$apply`/`$digest` and this is the one to investigate.
222+
223+
Once you have identified this call you work you way up the stack to see what the problem is.
224+
225+
* If the second call was made in your application code then you should look at why this code has been
226+
called from within a `$apply`/`$digest`. It may be a simple oversight or maybe it fits with the
227+
sync/async scenario described earlier.
71228

229+
* If the second call was made inside an Angular directive then it is likely that it matches the second
230+
programmatic event trigger scenario described earlier. In this case you may need to look further up
231+
the tree to what triggered the event in the first place.
232+
233+
### Example Problem
234+
235+
Let's investigate a problem with the following example code, which provides a new `setFocusIf`
236+
directive that will set the focus to the element on which it is defined when the value of its
237+
attribute becomes true.
238+
239+
<example name="error-$rootScope-inprog" module="app">
240+
<file name="index.html">
241+
<button ng-click="focusInput = true">Focus</button>
242+
<input ng-focus="count = count + 1" set-focus-if="focusInput" />
243+
</file>
244+
<file name="app.js">
245+
angular.module('app', []).directive('setFocusIf', function() {
246+
return function link($scope, $element, $attr) {
247+
$scope.$watch($attr.setFocusIf, function(value) {
248+
if ( value ) { $element[0].focus(); }
249+
});
250+
};
251+
});
252+
</file>
253+
</example>
254+
255+
When you click on the button to cause the focus to occur we get our `$rootScope:inprog` error. The
256+
stacktrace looks like this:
257+
258+
```
259+
Error: [$rootScope:inprog]
260+
at Error (native)
261+
at angular.min.js:6:467
262+
at n (angular.min.js:105:60)
263+
at g.$get.g.$apply (angular.min.js:113:195)
264+
at HTMLInputElement.<anonymous> (angular.min.js:198:401)
265+
at angular.min.js:32:32
266+
at Array.forEach (native)
267+
at q (angular.min.js:7:295)
268+
at HTMLInputElement.c (angular.min.js:32:14)
269+
at Object.fn (app.js:12:38) angular.js:10111
270+
(anonymous function) angular.js:10111
271+
$get angular.js:7412
272+
$get.g.$apply angular.js:12738 <--- $apply
273+
(anonymous function) angular.js:19833 <--- called here
274+
(anonymous function) angular.js:2890
275+
q angular.js:320
276+
c angular.js:2889
277+
(anonymous function) app.js:12
278+
$get.g.$digest angular.js:12469
279+
$get.g.$apply angular.js:12742 <--- $apply
280+
(anonymous function) angular.js:19833 <--- called here
281+
(anonymous function) angular.js:2890
282+
q angular.js:320
72283
```
73284

74-
To learn more about Angular processing model please check out the {@link guide/concepts concepts doc} as well as the {@link ng.$rootScope.Scope api} doc.
285+
We can see (even though the Angular code is minified) that there were two calls to `$apply`, first
286+
on line `19833`, then on line `12738` of `angular.js`.
287+
288+
It is this second call that caused the error. If we look at the angular.js code, we can see that
289+
this call is made by an Angular directive.
290+
291+
```
292+
var ngEventDirectives = {};
293+
forEach(
294+
'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
295+
function(name) {
296+
var directiveName = directiveNormalize('ng-' + name);
297+
ngEventDirectives[directiveName] = ['$parse', function($parse) {
298+
return {
299+
compile: function($element, attr) {
300+
var fn = $parse(attr[directiveName]);
301+
return function(scope, element, attr) {
302+
element.on(lowercase(name), function(event) {
303+
scope.$apply(function() {
304+
fn(scope, {$event:event});
305+
});
306+
});
307+
};
308+
}
309+
};
310+
}];
311+
}
312+
);
313+
```
314+
315+
It is not possible to tell which from the stack trace, but we happen know in this case that it is
316+
the `ngFocus` directive.
317+
318+
Now look up the stack to see that our application code is only entered once in `app.js` at line `12`.
319+
This is where our problem is:
320+
321+
```
322+
10: link: function($scope, $element, $attr) {
323+
11: $scope.$watch($attr.setFocusIf, function(value) {
324+
12: if ( value ) { $element[0].focus(); } <---- This is the source of the problem
325+
13: });
326+
14: }
327+
```
328+
329+
We can now see that the second appy was caused by us programmatically triggering a DOM event to
330+
occur. We must fix this by moving the code outside of the $apply block using `$$postDigest` or
331+
`$timeout` as described above.
332+
333+
## Further Reading
334+
To learn more about Angular processing model please check out the
335+
{@link guide/concepts concepts doc} as well as the {@link ng.$rootScope.Scope api} doc.

0 commit comments

Comments
 (0)