A better safeApply with Angular 1.5



Many of you might have run into this error at some point in time:

Error: $digest already in progress

This is typically created when someone calls $scope.$apply() in the middle of their code. Angular can be finicky about calling $apply when the $digest loop (2) is being executed. The error is essentially saying “I’m already $applying a bunch of stuff, I can’t add any more to the list right now.”

You might have searched for the issue and found a workaround solution like the one described here. This one shows a “safeApply” method that identifies the phase of the $digest loop and attempts to execute the function if it’s within the $apply or $digest phase, otherwise it applies the function using the $apply method.

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

While this code performs beautifully, allowing you to call $scope.safeApply() instead of $scope.$apply() to see the changes in your layout immediately, it’s not as succinct as it could be.

Here’s a better way to accomplish the same goal.

angular.module('MyModule')
.controller('MyController', function($scope, $timeout){
  $scope.myVar = "Hello";

  $scope.fixedApplyFunction = function(){
    // Use $timeout() instead of $scope.$apply() or $scope.safeApply().
    $timeout(function(){
        $scope.myVar = "world";
    });
  };
});

Why does $timeout work the same as safeApply()? Well, $timeout is an Angular service that defers function execution and executes that function at a specified time (in simplistic terms, but the code is a little more involved). If you don’t specify a time parameter then it executes the function immediately, also executing a full digest loop thereby updating your layout with any resulting changes.

Using $timeout also enables better unit tests because you can use $timeout.flush() to immediately execute these $timeout tasks and review your test results using expects.

My recommendation is to avoid the use of $scope.$apply() as much as possible. There’s rarely a need to use it, and if you find yourself needing it then you should know exactly why, and consider other alternatives such as $timeout() first.

Leave a Reply

Your email address will not be published. Required fields are marked *