Last Updated: March 21, 2023
·
140.4K
· mrvdot

'Safe' $apply in Angular.JS

If you find yourself triggering the '$apply already in progress' error while developing with Angular.JS (for me I find I hit most often when integrating third party plugins that trigger a lot of DOM events), you can use a 'safeApply' method that checks the current phase before executing your function. I usually just monkey patch this into the $scope object of my topmost controller, and Angular is nice enough to propagate it throughout the rest of my application for me:

$scope.safeApply = function(fn) {
  var phase = this.$root.$$phase;
  if(phase == '$apply' || phase == '$digest') {
    if(fn && (typeof(fn) === 'function')) {
      fn();
    }
  } else {
    this.$apply(fn);
  }
};

And then just replace $apply with safeApply wherever you need it

$scope.safeApply(function() {
  alert('Now I'm wrapped for protection!');
});

16 Responses
Add your response

Angular's apply is able to run without a function as a parameter, so it will propagate all previous changes to the UI.
I purpose this update:
$scope.safeApply = function(fn) {
var phase = this.$root.$$phase;
if(phase == '$apply' || phase == '$digest') {
if(fn)
fn();
} else {
this.$apply(fn);
}
};

over 1 year ago ·

@roypeled Thanks for the note, I hadn't realized apply could be called without a parameter. I've updated by original code to take that into account.

over 1 year ago ·

Have you considered submitting this to core?

over 1 year ago ·

@digger69 I have considered it and have been looking through repository to see where it would best fit. I'll update this thread once I submit a pull request.

over 1 year ago ·

Here's safeApply as an Angular service you can attach to your module. Additionally, this version accounts for calls to $apply() that don't pass in a function. To use it, attach the following to your module:

.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();
            }
        }
    }
}])

and access it with dependency injection:

.controller('MyCtrl', ['$scope,' 'safeApply', function($scope, safeApply) {
    safeApply($scope);                     // no function passed in
    safeApply($scope, function() {   // passing a function in
    });
}])
over 1 year ago ·

An easier way is to wrap the function in an $timeout without delay:

$timeout(function(){
// Do something
});

over 1 year ago ·

@marklagendijk That will likely work the vast majority of the time, though, in my experience, if you're responding to tons of events (such as a video feed), it still has the potential to result in multiple digests/applies.

More importantly, it results in your code being called after the current process thread. For many situations that may be preferable (particularly when doing DOM binding, such as with a jQuery plugin), but if you intend to immediately use any of the variables you just worked on, it will cause problems.

over 1 year ago ·

But this is bad :).
Don't do that.

over 1 year ago ·

That example wins.

over 1 year ago ·

Doing this with a service (as suggested above @andrewreutter) comes far too late for some use cases. Be a cool cat on the block, and use a decorator to configure this VERY early in your applications bootstrap phase before any module's services, directives, etc may try to access it before its available :)

yourAwesomeModule.config([
  '$provide', function($provide) {
    return $provide.decorator('$rootScope', [
      '$delegate', function($delegate) {
        $delegate.safeApply = function(fn) {
          var phase = $delegate.$$phase;
          if (phase === "$apply" || phase === "$digest") {
            if (fn && typeof fn === 'function') {
              fn();
            }
          } else {
            $delegate.$apply(fn);
          }
        };
        return $delegate;
      }
    ]);
  }
]);
over 1 year ago ·

Genius!

over 1 year ago ·

Question: Why use this.$root instead of just using $rootScope? Is there a difference? That way I can write a service that is independent of the current $scope.

over 1 year ago ·

There could be a race condition in here... What if my function execution ends after the current .$apply has called $diggest in order to refresh the scope? This is the case when we call .safeApply when something is already happening on .$apply, as you can see on the conditional, it will just simply execute the function.

over 1 year ago ·

You should almost always know if you're in AngularJS context or not, so this should be used only in some corner cases.

Triggering digest cycle too often won't do good for your app's performance. Lots of operations can and should be done outside (like scrolling / resizing handlers)

over 1 year ago ·

What if in function callback that has been provided to safeApply contains some $scope changes that angular already digested it. And at the meantime not all the scope variable digest has been finished yet. So in that case we will have (phase == '$apply' || phase == '$digest') == true and the changes in function provided will not take effect. Maybe I miss something.

over 1 year ago ·

this did not work. I am using upload-care angular and am guessing there are a ton of events that still cause the error mentioned. TImeout worked for me.

over 1 year ago ·