Angular $scope.$apply vs $timeout as a safe $apply

I'm trying to better understand the nuances of using the $timeout service in Angular as a sort of "safe $apply" method. Basically in scenarios where a piece of code could run in response to either an Angular event or a non-angular event such as jQuery or some standard DOM event.

As I understand things:

  1. Wrapping code in $scope.$apply works fine for scenarios where you aren't already in a digest loop (aka. jQuery event) but will raise an error if a digest is in progress
  2. Wrapping code in a $timeout() call with no delay parameter works whether already in a digest cycle or not

Looking at Angular source code, it looks like $timeout makes a call to $rootScope.$apply().

  1. Why doesn't $timeout() also raise an error if a digest cycle is already in progress?
  2. Is the best practice to use $scope.$apply() when you know for sure that a digest won't already be in progress and $timeout() when needing it to be safe either way?
  3. Is $timeout() really an acceptable "safe apply", or are there gotchas?

Thanks for any insight.


Solution 1:

Looking at Angular source code, it looks like $timeout makes a call to $rootScope.$apply().

  • Why doesn't $timeout() also raise an error if a digest cycle is already in progress?

$timeout makes use of an undocumented Angular service $browser. Specifically it uses $browser.defer() that defers execution of your function asynchronously via window.setTimeout(fn, delay), which will always run outside of Angular life-cycle. Only once window.setTimeout has fired your function will $timeout call $rootScope.$apply().

  • Is the best practice to use $scope.$apply() when you know for sure that a digest won't already be in progress and $timeout() when needing it to be safe either way?

I would say so. Another use case is that sometimes you need to access a $scope variable that you know will only be initialized after digest. Simple example would be if you want to set a form's state to dirty inside your controller constructor (for whatever reason). Without $timeout the FormController has not been initialized and published onto $scope, so wrapping $scope.yourform.setDirty() inside $timeout ensures that FormController has been initialized. Sure you can do all this with a directive without $timeout, just giving another use case example.

  • Is $timeout() really an acceptable "safe apply", or are there gotchas?

It should always be safe, but your go to method should always aim for $apply() in my opinion. The current Angular app I'm working on is fairly large and we've only had to rely on $timeout once instead of $apply().

Solution 2:

If we use $apply heavily in the application, we might get the Error: $digest already in progress. It happens because one $digest cycle can be run at a time. We can resolve it by $timeout or by $evalAsync.

The $timeout does not generate error like "$digest already in progress“ because $timeout tells Angular that after the current cycle, there is a timeout waiting and this way it ensures that there will not any collisions between digest cycles and thus output of $timeout will execute on a new $digest cycle.

I tried to explain them at : Comparison of apply, timeout,digest and evalAsync.

May be it will help you.

Solution 3:

As far as I understand it, $timeout is a wrapper around setTimeout which implicitly calls $scope.$apply, meaning it runs outside of the angular lifecycle, but kickstarts the angular lifecycle itself. The only "gotcha" I can think of is that if you're expecting your result to be available this $digest, you need to find another way to "safe apply" (which, AFAIK, is only available via $scope.$$phase).