|
3 | 3 | @fullName Action Already In Progress
|
4 | 4 | @description
|
5 | 5 |
|
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. |
8 | 10 |
|
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 |
10 | 12 |
|
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) |
12 | 70 |
|
13 | 71 | This error is often seen when interacting with an API that is sometimes sync and sometimes async.
|
14 | 72 |
|
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. |
16 | 76 |
|
17 | 77 | ```
|
18 |
| -function MyController() { |
| 78 | +function MyController($scope, thirdPartyComponent) { |
19 | 79 | thirdPartyComponent.getData(function(someData) {
|
20 |
| - scope.$apply(function() { |
21 |
| - scope.someData = someData; |
| 80 | + $scope.$apply(function() { |
| 81 | + $scope.someData = someData; |
22 | 82 | });
|
23 | 83 | });
|
24 | 84 | }
|
25 | 85 | ```
|
26 | 86 |
|
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`. |
28 | 89 |
|
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. |
30 | 93 |
|
| 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. |
31 | 96 |
|
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. |
33 | 98 |
|
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. |
35 | 101 |
|
36 | 102 | ```
|
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() { |
38 | 126 | 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 | + }); |
48 | 131 | }
|
49 |
| - } |
| 132 | + }; |
50 | 133 | });
|
| 134 | +``` |
51 | 135 |
|
| 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> |
52 | 142 | ```
|
53 | 143 |
|
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 | + |
55 | 166 | ```
|
56 |
| -myApp.directive('myDirective', function() { |
| 167 | +myApp.directive('setFocusIf', function() { |
57 | 168 | 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 | + } |
65 | 180 | });
|
| 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: |
66 | 189 |
|
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 | + }); |
68 | 205 | }
|
69 | 206 | }
|
70 | 207 | });
|
| 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. |
71 | 228 |
|
| 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 |
72 | 283 | ```
|
73 | 284 |
|
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