Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with:

$scope.$evalAsync() vs. $timeout() In AngularJS

By Ben Nadel on

Sometimes, in an AngularJS application, you have to explicitly tell AngularJS when to initiate it's $digest() lifecycle (for dirty-data checking). This requirement is typically contained within a Directive; but, it may also be in an asynchronous Service. Most of the time, this can be easily accomplished with the $scope.$apply() method. However, some of the time, you have to defer the $apply() invocation because it may or may not conflict with an already-running $digest phase. In those cases, you can use the $timeout() service; but, I'm starting to think that the $scope.$evalAsync() method is a better option.

Generally speaking, it's clear as to whether or not an AngularJS $digest is already executing. But, sometimes, depending on the context, this distinction becomes blurry. Consider the following pseudo-code for a Directive link() function:

  • // PSEUDO-CODE for AngularJS directive link function.
  • function link( $scope ) {
  •  
  • function handler( data ) {
  • $scope.$apply(
  • function() {
  • // ...
  • }
  • );
  • }
  •  
  •  
  • if ( cachedData ) {
  •  
  • handler( cachedData );
  •  
  • } else {
  •  
  • getDataAsync( handler );
  •  
  • }
  •  
  • }

Here, we are working with data that may or may not be cached locally. If it's cached, we use it immediately; if it's not cached, we get it asynchronously. This duality causes a problem for the data handler. In one context - the cached data - the handler is called within the lifecycle of an active $digest. Then, in the other context - the asynchronous get - the handler is called outside of an AngularJS $digest.

This means that some of the time, the directive will work properly; and, some of the time, it will throw the following error:

Error: $digest already in progress

To side-step this problem, we either put in logic that explicitly checks the AngularJS $$phase (which is a big no-no!); or, we make sure that the callback handler initiates a $digest at a later time.

Up until now, my approach to deferred-$digest-invocation was to replace the $scope.$apply() call with the $timeout() service (which implicitly calls $apply() after a delay). But, yesterday, I discovered the $scope.$evalAsync() method. Both of these accomplish the same thing - they defer expression-evaluation until a later point in time. But, the $scope.$evalAsync() is likely to execute in the same tick of the JavaScript event loop.

Take a look at the following code. Notice that there are two calls to $timeout() that sandwich a call to $scope.$evalAsync():

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • $scope.$evalAsync() vs. $timeout() In AngularJS
  • </title>
  • </head>
  • <body>
  •  
  • <h1>
  • $scope.$evalAsync() vs. $timeout() In AngularJS
  • </h1>
  •  
  • <p bn-timing>
  • Check the console!
  • </p>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/jquery/jquery-2.0.3.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.2.4.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Test the timing of the $timeout() and $evalAsync() functions.
  • app.directive(
  • "bnTiming",
  • function( $timeout ) {
  •  
  • // I bind the JavaScript events to the local scope.
  • function link( $scope, element, attributes ) {
  •  
  • $timeout(
  • function() {
  •  
  • console.log( "$timeout 1" );
  •  
  • }
  • );
  •  
  • $scope.$evalAsync(
  • function( $scope ) {
  •  
  • console.log( "$evalAsync" );
  •  
  • }
  • );
  •  
  • $timeout(
  • function() {
  •  
  • console.log( "$timeout 2" );
  •  
  • }
  • );
  •  
  • }
  •  
  •  
  • // Return the directive configuration.
  • return({
  • link: link
  • });
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

When we run this code, we get the following console output:

$evalAsync
$timeout 1
$timeout 2

Run this demo in my JavaScript Demos project on GitHub.

Even though the first $timeout() call was before the $scope.$evalAsync() method, you can see that the $scope.$evalAsync() expression was evaluated first. This is because the $scope.$evalAsync() expressions are placed in an "async queue" that is flushed at the start of each $digest iteration. As a very high level, the $digest loop looks like this:

  • Do:
  • - - - If asyncQueue.length, flush asyncQueue.
  • - - - Trigger all $watch handlers.
  • - - - Check for "too many" $digest iterations.
  • While: ( Dirty data || asyncQueue.length )

If some aspect of the $digest phase adds an expressions to the asyncQueue (using $scope.$evalAsync()), AngularJS will perform another iteration of the $digest loop in order to flush the asyncQueue. This way, your expression is very likely to be evaluated in the same tick of the JavaScript event loop.

Of course, there are outlier cases where this isn't true, such as if the $scope.$evalAsync() puts the $digest loop over its "max iterations" limit or another expression throws an error. This is why AngularJS also uses a timeout in the $scope.$evalAsync() method. In addition to updating the asyncQueue, AngularJS also initiates a timeout that checks the asyncQueue length. This way, if the asyncQueue isn't flushed during the current $digest cycle, it will surely be flushed in a later tick of the event loop.

So, in essence, $scope.$evalAsync() combines the best of both worlds: When it can (which is most of the time), it will evaluate your expression in the same tick; otherwise, it will evaluate your expression in a later tick, which is exactly what $timeout() is doing.

I'm not saying that all instances of $timeout() should be replaced with $scope.$evalAsync() - they serve two different, albeit related, purposes. If you truly want to execute code at a later point in time, use $timeout(). However, if your only goal is tell AngularJS about a data change without throwing a "$digest already in progress" error, I would suggest using $scope.$evalAsync().




Reader Comments

Oh very interesting, thanks for the tip.
Besides, I think that using a timeout for that kind of things seems a bit like a dirty hack (kinda like the safeApply).
This seems more likely to be the correct intended usage :)

Reply to this Comment

@All,

***Important Node:*** The additional "timeout" that is performed in addition to the asyncQueue was not added until v1.2 of AngularJS. As such, if you try to use $evalAsync() before v1.2, you might not see the changes take place until something else explicitly performs a digest.

Reply to this Comment

@Kelly,

Really glad to be able to help - but just take note that AngularJS 1.2 made an important change to the way $evalAsync() works (see above comment). Prior to 1.2, it didn't add the "defer" fallback. So, if you are pre-1.2, switching to $evalAsync() will cause some problems.

Reply to this Comment

@Demetrius,

Dealing with the DOM and knowing when it has updated is some really interesting stuff. First off, never worry about the DOM from a Controller - the controller should not know anything about the DOM. Really, only the Directives should know about the DOM. And, if you truly want to be sure that the DOM has updated, then, yeah, using a $timeout() is probably the only fool-proof approach since the callback will be called in a later tick of the event loop.

"Watching" DOM rendering is a really complex topic. $watch() callbacks are invoked in the order in which they are bound. This gets fun! This means that, depending on your app was put together, two $watch() handlers that fire in the same digest may actually have different versions of the DOM available. Fun stuff!

Reply to this Comment

Ben, how controller or directive with $evalAsync can be tested? In case of usage $timeout I can use $timeout.flush() in my tests to force an execution of delayed actions.

Reply to this Comment

@Kelly,

But if function will be called asynchronously, in this case $scope.$digest doesn't fit me.
I mean that scope.$digest will trigger the digest cycle, but function passed to the evalAsync queue could be called inside $timeout func, within the next tick.

Reply to this Comment

Hey Ben, i've been replacing all my calls to $timeout(fn) to scope.$evalAsync(fn) and i found that i had to revert a couple of those changes because "$digest already in progress" errors appeared. Why is that? Isn't this supposed to be a workaround for that? Changing that back to $timeout() calls, worked ok. Again, it happened for a couple of them. Any ideas?

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.