diff --git a/docs/content/error/$rootScope/inprog.ngdoc b/docs/content/error/$rootScope/inprog.ngdoc index 375f8fe803f2..603a4f4db4c7 100644 --- a/docs/content/error/$rootScope/inprog.ngdoc +++ b/docs/content/error/$rootScope/inprog.ngdoc @@ -3,72 +3,310 @@ @fullName Action Already In Progress @description -At any point in time there can be only one `$digest` or $apply operation in progress. -The stack trace of this error allows you to trace the origin of the currently executing $apply or $digest call. +At any point in time there can be only one `$digest` or `$apply` operation in progress. This is to +prevent very hard to detect bugs from entering your application. The stack trace of this error +allows you to trace the origin of the currently executing `$apply` or `$digest` call, which caused +the error. -`$digest` or `$apply` are processing operational states of the Scope - data-structure in Angular that provides context for models and enables model mutation observation. +## Background -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. +Angular uses a dirty-checking digest mechanism to monitor and update values of the scope during +the processing of your application. The digest works by checking all the values that are being +watched against their previous value and running any watch handlers that have been defined for those +values that have changed. + +This digest mechanism is triggered by calling `$digest` on a scope object. Normally you do not need +to trigger a digest manually, because every external action that can trigger changes in your +application, such as mouse events, timeouts or server responses, wrap the Angular application code +in a block of code that will run `$digest` when the code completes. + +You wrap Angular code in a block that will be followed by a `$digest` by calling `$apply` on a scope +object. So, in pseudo-code, the process looks like this: + +``` +element.on('mouseup', function() { + scope.$apply(function() { + $scope.doStuff(); + }); +}); +``` + +where `$apply()` looks something like: + +``` +$apply = function(fn) { + try { + fn(); + } finally() { + $digest(); + } +} +``` + +## Digest Phases + +Angular keeps track of what phase of processing we are in, the relevant ones being `$apply` and +`$digest`. 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. So Angular will throw this error when +that occurs. + +In most situations it should be well defined whether a piece of code will be run inside an `$apply`, +in which case you should not be calling `$apply` or `$digest`, or it will be run outside, in which +case you should wrap any code that will be interacting with Angular scope or services, in a call to +`$apply`. + +As an example, all Controller code should expect to be run within Angular, so it should have no need +to call `$apply` or `$digest`. Conversely, code that is being trigger directly as a call back to +some external event, from the DOM or 3rd party library, should expect that it is never called from +within Angular, and so any Angular application code that it calls should first be wrapped in a call +to $apply. + +## Common Causes + +Apart from simply incorrect calls to `$apply` or `$digest` there are some cases when you may get +this error through no fault of your own. + +### Inconsistent API (Sync/Async) This error is often seen when interacting with an API that is sometimes sync and sometimes async. -For example: +For example, imagine a 3rd party library that has a method which will retrieve data for us. Since it +may be making an asynchronous call to a server, it accepts a callback function, which will be called +when the data arrives. ``` -function MyController() { +function MyController($scope, thirdPartyComponent) { thirdPartyComponent.getData(function(someData) { - scope.$apply(function() { - scope.someData = someData; + $scope.$apply(function() { + $scope.someData = someData; }); }); } ``` -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. +We expect that our callback will be called asynchronously, and so from outside Angular. Therefore, we +correctly wrap our application code that interacts with Angular in a call to `$apply`. -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. +The problem comes if `getData()` decides to call the callback handler synchronously; perhaps it has +the data already cached in memory and so it immediately calls the callback to return the data, +synchronously. +Since, the `MyController` constructor is always instantiated from within an `$apply` call, our +handler is trying to enter a new `$apply` block from within one. -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. +This is not an ideal design choice on the part of the 3rd party library. -For example: +To resolve this type of issue, either fix the api to be always synchronous or asynchronous or force +your callback handler to always run asynchronously by using the `$timeout` service. ``` -myApp.directive('myDirective', function() { +function MyController($scope, thirdPartyComponent) { + thirdPartyComponent.getData(function(someData) { + $timeout(function() { + $scope.someData = someData; + }, 0); + }); +} +``` + +Here we have used `$timeout` to schedule the changes to the scope in a future call stack. +By providing a timeout period of 0ms, this will occur as soon as possible and `$timeout` will ensure +that the code will be called in a single `$apply` block. + +### Triggering Events Programmatically + +The other situation that often leads to this error is when you trigger code (such as a DOM event) +programmatically (from within Angular), which is normally called by an external trigger. + +For example, consider a directive that will set focus on an input control when a value in the scope +is true: + +``` +myApp.directive('setFocusIf', function() { return { - link: function($scope, $element) { - function doSomeWork() { - $scope.$apply(function() { - // do work here, and update the model - }; - } - - $element.on('click', doSomeWork); - doSomeWork(); // << this will throw an exception because templates are compiled within $apply + link: function($scope, $element, $attr) { + $scope.$watch($attr.setFocusIf, function(value) { + if ( value ) { $element[0].focus(); } + }); } - } + }; }); +``` +If we applied this directive to an input which also used the `ngFocus` directive to trigger some +work when the element receives focus we will have a problem: + +``` + + ``` -The fix for the example above looks like this: +In this setup, there are two ways to trigger ngFocus. First from a user interaction: + +* Click on the input control +* The input control gets focus +* The `ngFocus` directive is triggered, setting `$scope.msg='has focus'` from within a new call to +`$apply()` + +Second programmatically: + +* Click the button +* The `ngClick` directive sets the value of `$scope.hasFocus` to true inside a call to `$apply` +* The `$digest` runs, which triggers the watch inside the `setFocusIf` directive +* The watch's handle runs, which gives the focus to the input +* The `ngFocus` directive is triggered, setting `$scope.msg='has focus'` from within a new call to +`$apply()` + +In this second scenario, we are already inside a `$digest` when the ngFocus directive makes another +call to `$apply()`, causing this error to be thrown. + +It is possible to workaround this problem by moving the call to set the focus outside of the digest, +by using `$timeOut(fn, 0, false)`, where the `false` value tells Angular not to wrap this `fn` in a +`$apply` block: + ``` -myApp.directive('myDirective', function() { +myApp.directive('setFocusIf', function($timeout) { return { - link: function($scope, $element) { - function doSomeWork() { - // do work here, and update the model - } - - $element.on('click', function() { - $scope.$apply(doSomeWork); // <<< the $apply call was moved to the callsite that doesn't execute in $apply call already + link: function($scope, $element, $attr) { + $scope.$watch($attr.setFocusIf, function(value) { + if ( value ) { + $timeout(function() { + // We must reevaluate the value in case it was changed by a subsequent + // watch handler in the digest. + if ( $scope.$eval($attr.setFocusIf) ) { + $element[0].focus(); + } + }, 0, false); + } }); - - doSomeWork(); } } }); +``` + +## Diagnosing This Error + +When you get this error it can be rather daunting to diagnose the cause of the issue. The best +course of action is to investigate the stack trace from the error. You need to look for places +where `$apply` or `$digest` have been called and find the context in which this occurred. + +There should be two calls: + +* The first call is the good `$apply`/`$digest` and would normally be triggered by some event near +the top of the call stack. + +* The second call is the bad `$apply`/`$digest` and this is the one to investigate. + +Once you have identified this call you work you way up the stack to see what the problem is. + +* If the second call was made in your application code then you should look at why this code has been +called from within a `$apply`/`$digest`. It may be a simple oversight or maybe it fits with the +sync/async scenario described earlier. + +* If the second call was made inside an Angular directive then it is likely that it matches the second +programmatic event trigger scenario described earlier. In this case you may need to look further up +the tree to what triggered the event in the first place. + +### Example Problem + +Let's look at how to investigate this error using the `setFocusIf` example from above. This example +defines a new `setFocusIf` directive that sets the focus on the element where it is defined when the +value of its attribute becomes true. + + + + + + + + angular.module('app', []).directive('setFocusIf', function() { + return function link($scope, $element, $attr) { + $scope.$watch($attr.setFocusIf, function(value) { + if ( value ) { $element[0].focus(); } + }); + }; + }); + + + +When you click on the button to cause the focus to occur we get our `$rootScope:inprog` error. The +stacktrace looks like this: + +``` +Error: [$rootScope:inprog] + at Error (native) + at angular.min.js:6:467 + at n (angular.min.js:105:60) + at g.$get.g.$apply (angular.min.js:113:195) + at HTMLInputElement. (angular.min.js:198:401) + at angular.min.js:32:32 + at Array.forEach (native) + at q (angular.min.js:7:295) + at HTMLInputElement.c (angular.min.js:32:14) + at Object.fn (app.js:12:38) angular.js:10111 +(anonymous function) angular.js:10111 +$get angular.js:7412 +$get.g.$apply angular.js:12738 <--- $apply +(anonymous function) angular.js:19833 <--- called here +(anonymous function) angular.js:2890 +q angular.js:320 +c angular.js:2889 +(anonymous function) app.js:12 +$get.g.$digest angular.js:12469 +$get.g.$apply angular.js:12742 <--- $apply +(anonymous function) angular.js:19833 <--- called here +(anonymous function) angular.js:2890 +q angular.js:320 +``` + +We can see (even though the Angular code is minified) that there were two calls to `$apply`, first +on line `19833`, then on line `12738` of `angular.js`. + +It is this second call that caused the error. If we look at the angular.js code, we can see that +this call is made by an Angular directive. ``` +var ngEventDirectives = {}; +forEach( + 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '), + function(name) { + var directiveName = directiveNormalize('ng-' + name); + ngEventDirectives[directiveName] = ['$parse', function($parse) { + return { + compile: function($element, attr) { + var fn = $parse(attr[directiveName]); + return function(scope, element, attr) { + element.on(lowercase(name), function(event) { + scope.$apply(function() { + fn(scope, {$event:event}); + }); + }); + }; + } + }; + }]; + } +); +``` + +It is not possible to tell which from the stack trace, but we happen to know in this case that it is +the `ngFocus` directive. + +Now look up the stack to see that our application code is only entered once in `app.js` at line `12`. +This is where our problem is: + +``` +10: link: function($scope, $element, $attr) { +11: $scope.$watch($attr.setFocusIf, function(value) { +12: if ( value ) { $element[0].focus(); } <---- This is the source of the problem +13: }); +14: } +``` + +We can now see that the second `$apply` was caused by us programmatically triggering a DOM event +(i.e. focus) to occur. We must fix this by moving the code outside of the $apply block using +`$timeout` as described above. -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. +## Further Reading +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.