94

Sometimes I need to use $scope.$apply in my code and sometimes it throws a "digest already in progress" error. So I started to find a way around this and found this question: AngularJS : Prevent error $digest already in progress when calling $scope.$apply(). However in the comments (and on the angular wiki) you can read:

Don't do if (!$scope.$$phase) $scope.$apply(), it means your $scope.$apply() isn't high enough in the call stack.

So now i have two questions:

  1. Why exactly is this an anti-pattern?
  2. How can i safely use $scope.$apply?

Another "solution" to prevent "digest already in progress" error seems to be using $timeout:

$timeout(function() {
  //...
});

Is that the way to go? Is it safer? So here is the real question: How I can entirely eliminate the possibility of a "digest already in progress" error?

PS: I am only using $scope.$apply in non-angularjs callbacks that are not synchronous. (as far as I know those are situations where you must use $scope.$apply if you want your changes to be applied)

Community
  • 1
  • 1
Dominik Goltermann
  • 4,276
  • 2
  • 26
  • 32
  • From my experience, you should always know, if you are manipulating `scope` from within angular or from outside of angular. So according to this you always know, if you need to call `scope.$apply` or not. And if you are using the same code for both angular/non-angular `scope` manipulation, you're doing it wrong, it should be always separated... so basically if you run into a case where you need to check `scope.$$phase`, your code is not designed in a correct way, and there is always a way to do it 'the right way' – doodeec Mar 12 '14 at 09:36
  • 1
    i'm only using this in non-angular callbacks (!) This is why I am confused – Dominik Goltermann Mar 12 '14 at 09:40
  • 2
    if it was non-angular, it wouldn't throw `digest already in progress` error – doodeec Mar 12 '14 at 09:46
  • 1
    that's what i thought. The thing is: it doesn't always throw the error. Only once in a while. My suspection is that the apply collides BY CHANCE with another digest. Is that possible? – Dominik Goltermann Mar 12 '14 at 09:48
  • I don't think that is possible if the callback is strictly non-angular – doodeec Mar 12 '14 at 09:51
  • you're right, see my answer. The problem i was having must be somwhere else – Dominik Goltermann Mar 12 '14 at 11:15

6 Answers6

115

After some more digging i was able to solve the question whether it's always safe to use $scope.$apply. The short answer is yes.

Long answer:

Due to how your browser executes Javascript, it is not possible that two digest calls collide by chance.

The JavaScript code we write doesn’t all run in one go, instead it executes in turns. Each of these turns runs uninterupted from start to finish, and when a turn is running, nothing else happens in our browser. (from http://jimhoskins.com/2012/12/17/angularjs-and-apply.html)

Hence the error "digest already in progress" can only occur in one situation: When an $apply is issued inside another $apply, e.g.:

$scope.apply(function() {
  // some code...
  $scope.apply(function() { ... });
});

This situation can not arise if we use $scope.apply in a pure non-angularjs callback, like for example the callback of setTimeout. So the following code is 100% bulletproof and there is no need to do a if (!$scope.$$phase) $scope.$apply()

setTimeout(function () {
    $scope.$apply(function () {
        $scope.message = "Timeout called!";
    });
}, 2000);

even this one is safe:

$scope.$apply(function () {
    setTimeout(function () {
        $scope.$apply(function () {
            $scope.message = "Timeout called!";
        });
    }, 2000);
});

What is NOT safe (because $timeout - like all angularjs helpers - already calls $scope.$apply for you):

$timeout(function () {
    $scope.$apply(function () {
        $scope.message = "Timeout called!";
    });
}, 2000);

This also explains why the usage of if (!$scope.$$phase) $scope.$apply() is an anti-pattern. You simply don't need it if you use $scope.$apply in the correct way: In a pure js callback like setTimeout for example.

Read http://jimhoskins.com/2012/12/17/angularjs-and-apply.html for the more detailed explanation.

mzedeler
  • 4,177
  • 4
  • 28
  • 41
Dominik Goltermann
  • 4,276
  • 2
  • 26
  • 32
  • I got an example where I create a service with ```$document.bind('keydown', function(e) { $rootScope.$apply(function() { // a passed through function from the controller gets executed here }); });``` I really don't know why I have to make $apply here, because I'm using $document.bind.. – Betty St Mar 13 '14 at 07:59
  • because $document is only "A jQuery or jqLite wrapper for the browser's window.document object." and implemented as follows: ```function $DocumentProvider(){ this.$get = ['$window', function(window){ return jqLite(window.document); }]; }``` There is no apply in there. – Dominik Goltermann Mar 13 '14 at 11:14
  • ah thank you :) so one question is your first code example of a save apply like just using ``$timeout(function() {})`` ? – Betty St Mar 13 '14 at 13:51
  • 11
    `$timeout` semantically means running code after a delay. It might be a functionally safe thing to do but it is a hack. There should be a safe way to use $apply when you're unable to know whether a `$digest` cycle is in progress or you're already inside an `$apply`. – John Strickler Nov 25 '14 at 15:17
  • Thanks for your answer. I'd say the short answer should be "no", since there are cases that are not safe, like when you $apply inside an already $apply-ed function such as $timeout or functions called by attribute directives like ng-change. – Ferran Maylinch Feb 03 '15 at 15:15
  • FYI: `$timeout` has a 3rd argument which can be set to false to avoid invoking apply automatically `$timeout([fn], [delay], [invokeApply]);` (or you could just run it with `invokeApply: true` and not have to call it by yourself, but I guess that applies the `$rootScope` which may make more work for `$digest`?) – Jamie Pate Feb 05 '15 at 17:41
  • @JohnStrickler I agree. Any time you see a novice developer writing with timeouts in vanilla JS, its automatically a hack. The fact that we have to use $timeout set to 0 not only *feels* hacky, it just demonstrates that there's holes in the framework. At the very least there should be an API call that allows us to defer some script execution until after the current digest cycle has completed. Perhaps $scope.$apply should do this intrinsically. But I guess I don't understand it too well yet myself. – dudewad Aug 29 '15 at 07:51
  • here's a good reason that it's not an anti-pattern: http://jsfiddle.net/d48eqjcL/ some events fire synchronously if triggered from code. whereas if i click out and click back into the text box a digest will not be in progress. if you need something to be reflected synchronously in angular from within that callback the supposed anti-pattern is the only option. also i still haven't heard a good explanation for why this pattern is bad. only that it's usually unnecessary. would love to hear a reason it's actually a problem – Sterling Camden Nov 25 '15 at 19:10
  • 1
    another reason why its bad: it uses internal variables ($$phase) which are not part of the public api and they might be changed in a newer version of angular and thus break your code. Your problem with syncronous event triggering is interesting though – Dominik Goltermann Nov 26 '15 at 09:54
  • 4
    Newer approach is to use $scope.$evalAsync() which safely executes in current digest cycle if possible or in next cycle. Refer to http://www.bennadel.com/blog/2605-scope-evalasync-vs-timeout-in-angularjs.htm – jaymjarri Dec 16 '15 at 15:45
  • I'd say that the short answer is, "yes... if you use it properly". – Adam Zerner Mar 25 '16 at 17:13
17

It is most definitely an anti-pattern now. I've seen a digest blow up even if you check for the $$phase. You're just not supposed to access the internal API denoted by $$ prefixes.

You should use

 $scope.$evalAsync();

as this is the preferred method in Angular ^1.4 and is specifically exposed as an API for the application layer.

FlavorScape
  • 13,301
  • 12
  • 75
  • 117
  • Necro, but mind providing a source or further explanation around "I've seen a digest blow up even if you check for the $$phase. " – Jacob Barnes Feb 24 '21 at 18:37
10

In any case when your digest in progress and you push another service to digest, it simply gives an error i.e. digest already in progress. so to cure this you have two option. you can check for anyother digest in progress like polling.

First one

if ($scope.$root.$$phase != '$apply' && $scope.$root.$$phase != '$digest') {
    $scope.$apply();
}

if the above condition is true, then you can apply your $scope.$apply otherwies not and

second solution is use $timeout

$timeout(function() {
  //...
})

it will not let the other digest to start untill $timeout complete it's execution.

Lalit Sachdeva
  • 6,469
  • 2
  • 19
  • 25
  • 1
    downvoted; The question specifically asks why NOT to do the thing you are describing here, not for another way to hack around it. See the excellent answer by @gaul for when to use `$scope.$apply();`. – PureSpider Oct 27 '14 at 09:36
  • Though not answering the question: ````$timeout```` is the key! it works and later i found that it is recommended too. – Himel Nag Rana May 29 '15 at 09:21
  • I know it is quite late to add comment to this 2 years later, but be careful when using $timeout too much, as this can cost you too much in performance if you do not have good application structure – cpoDesign Jul 13 '16 at 14:48
9

scope.$apply triggers a $digest cycle which is fundamental to 2-way data binding

A $digest cycle checks for objects i.e. models(to be precise $watch) attached to $scope to assess if their values have changed and if it detects a change then it takes necessary steps to update the view.

Now when you use $scope.$apply you face an error "Already in progress" so it is quite obvious that a $digest is running but what triggered it?

ans--> every $http calls, all ng-click, repeat, show, hide etc trigger a $digest cycle AND THE WORST PART IT RUNS OF EVERY $SCOPE.

ie say your page has 4 controllers or directives A,B,C,D

If you have 4 $scope properties in each of them then you have a total of 16 $scope properties on your page.

If you trigger $scope.$apply in controller D then a $digest cycle will check for all 16 values!!! plus all the $rootScope properties.

Answer-->but $scope.$digest triggers a $digest on child and same scope so it will check only 4 properties. So if you are sure that changes in D will not affect A, B, C then use $scope.$digest not $scope.$apply.

So a mere ng-click or ng-show/hide might be triggering a $digest cycle on over 100+ properties even when the user has not fired any event!

Tamas Rev
  • 7,008
  • 5
  • 32
  • 49
Rishul Matta
  • 3,383
  • 5
  • 23
  • 29
  • 2
    Yeah I realized this late into the project unfortunately. Wouldn't have used Angular if I knew this from the start. All standard directives fire a $scope.$apply, which in turns calls $rootScope.$digest, which performs dirty checks on ALL the scopes. Poor design decision if you ask me. I should be in control of what scopes should be dirty checked, because I KNOW HOW THE DATA IS LINKED TO THESE SCOPES! – MoonStom Jul 14 '14 at 10:34
0

Use $timeout, it is the way recommended.

My scenario is that I need to change items on the page based on the data I received from a WebSocket. And since it is outside of Angular, without the $timeout, the only model will be changed but not the view. Because Angular doesn't know that piece of data has been changed. $timeout is basically telling Angular to make the change in the next round of $digest.

I tried the following as well and it works. The difference to me is that $timeout is clearer.

setTimeout(function(){
    $scope.$apply(function(){
        // changes
    });
},0)
Sunil Garg
  • 14,608
  • 25
  • 132
  • 189
James J. Ye
  • 457
  • 2
  • 11
  • It's much cleaner to wrap your socket code in $apply (much like Angular's on AJAX code, i.e `$http`). Otherwise you have to repeat this code all over the place. – timruffs Jul 30 '15 at 15:01
  • this is definitely not recommended. Also, you will occasionally get an error doing this if $scope has $$phase. instead, you should use $scope.$evalAsync(); – FlavorScape Apr 28 '16 at 19:42
  • There is no need of `$scope.$apply` if you are using `setTimeout` or `$timeout` – Kunal May 10 '17 at 20:03
-1

I found very cool solution:

.factory('safeApply', [function($rootScope) {
    return function($scope, fn) {
        var phase = $scope.$root.$$phase;
        if (phase == '$apply' || phase == '$digest') {
            if (fn) {
                $scope.$eval(fn);
            }
        } else {
            if (fn) {
                $scope.$apply(fn);
            } else {
                $scope.$apply();
            }
        }
    }
}])

inject that where you need:

.controller('MyCtrl', ['$scope', 'safeApply',
    function($scope, safeApply) {
        safeApply($scope); // no function passed in
        safeApply($scope, function() { // passing a function in
        });
    }
])
bora89
  • 3,476
  • 1
  • 25
  • 16