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 CFinNC 2009 (Raleigh, North Carolina) with:

Don't Forget To Cancel $timeout Timers In Your $destroy Events In AngularJS

By Ben Nadel on

Unfortunately, people seem to treat the $timeout() function in AngularJS as a set-it and forget-it type of function. But, forgetting about your $timeout callback can have negative consequences, all the way from code that fails silently to code that raises an exception to code that makes repeated $http requests to your server for no reason. The trick to managing your $timeout timers properly is to cancel them in your $destroy events.


 
 
 

 
  
 
 
 

View this demo in my JavaScript-Demos project on GitHub.

Unlike the core JavaScript setTimeout() and setInterval() functions, the $timeout() function in AngularJS returns a promise. And, just like any other promise, you can bind to the $timeout's resolved and rejected events. More importantly, however, you can cancel the underlying timer by passing the promise off to the $timeout.cancel() method.

In an AngularJS application, this becomes very important because timers can end up executing code that is no longer relevant to the state of the application and the user interface. At best, this happens silently; at worse, this causes unexpected behavior that leads to a poor user experience. To keep things running smoothly, I recommend that you always keep a handle on your $timeout timers; and, that you call the $timeout.cancel() method anytime the containing Controller or Directive receives the $destroy event.

To see this in action, I have a few DOM elements below that are created and destroyed using the ngSwitch/ngSwitchWhen directives. Notice that when the $destroy event is triggered (this case, in our Directive), I am canceling the current timer.

  • <!doctype html>
  • <html ng-app="Demo" ng-controller="DemoController">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Don't Forget To Cancel $timeout Timers In Your $destroy Events In AngularJS
  • </title>
  • </head>
  • <body>
  •  
  • <h1>
  • Don't Forget To Cancel $timeout Timers In Your $destroy Events In AngularJS
  • </h1>
  •  
  • <p>
  • <a href="#" ng-click="toggle()">Toggle Section</a>
  • </p>
  •  
  • <div ng-switch="section">
  •  
  • <p ng-switch-when="happy" bn-directive>
  • Oh sweet!
  • </p>
  •  
  • <p ng-switch-when="sad" bn-directive>
  • Oh noes!
  • </p>
  •  
  • </div>
  •  
  •  
  • <!-- Load jQuery and AngularJS. -->
  • <script
  • type="text/javascript"
  • src="../../vendor/jquery/jquery-2.0.3.min.js">
  • </script>
  • <script
  • type="text/javascript"
  • src="../../vendor/angularjs/angular-1.0.7.min.js">
  • </script>
  • <script type="text/javascript">
  •  
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I control the main demo.
  • app.controller(
  • "DemoController",
  • function( $scope ) {
  •  
  • $scope.section = "happy";
  •  
  • // I toggle the section value, to show/hide the
  • // differnet sections in the markup.
  • $scope.toggle = function() {
  •  
  • if ( $scope.section === "happy" ) {
  •  
  • $scope.section = "sad";
  •  
  • } else {
  •  
  • $scope.section = "happy";
  •  
  • }
  •  
  • };
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I'm just a sample directove to demonstrate the clearing
  • // of a $timeout event in the AngularJS $destroy event.
  • app.directive(
  • "bnDirective",
  • function( $timeout ) {
  •  
  • // I bind the User Interface events to the $scope.
  • function link( $scope, element, attributes ) {
  •  
  • // When the timeout is defined, it returns a
  • // promise object.
  • var timer = $timeout(
  • function() {
  •  
  • console.log( "Timeout executed", Date.now() );
  •  
  • },
  • 2000
  • );
  •  
  •  
  • // Let's bind to the resolve/reject handlers of
  • // the timer promise so that we can make sure our
  • // cancel approach is actually working.
  • timer.then(
  • function() {
  •  
  • console.log( "Timer resolved!", Date.now() );
  •  
  • },
  • function() {
  •  
  • console.log( "Timer rejected!", Date.now() );
  •  
  • }
  • );
  •  
  •  
  • // When the DOM element is removed from the page,
  • // AngularJS will trigger the $destroy event on
  • // the scope. This gives us a chance to cancel any
  • // pending timer that we may have.
  • $scope.$on(
  • "$destroy",
  • function( event ) {
  •  
  • $timeout.cancel( timer );
  •  
  • }
  • );
  •  
  • }
  •  
  • // Return the directive configuration.
  • return({
  • link: link,
  • scope: false
  • });
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

Most of the time, letting your $timeout timer run its course probably won't lead to a negative outcome. But, I suggest getting into the practice of clearing $timeout's since they can lead to a poor user experience (or apply unnecessary load to your server). This applies to both Controllers and Directives.




Reader Comments

Hi,
I came cross this great page, what I was looking for is to verify the $destroy event can be properly fired as expected, I copy you code and add some log to console in the $destroy event, but nothing happens.

$scope.$on(
"$destroy",
function( event ) {
$timeout.cancel( timer );
console.log( "Timer Canceled!", Date.now() );
}
);

Can you explain?

Thanks
John

Reply to this Comment

@John,

I am sorry, I do not understand what you are asking? AngularJS will implicitly trigger the $destroy event when the relevant view is destroyed. In the majority of cases, you won't ever have to trigger $destroy yourself.

The exception to this is when you are creating Directives that implement transclusion. In those cases, you may have to explicitly call $scope.$destroy() - but, again, very unlikely for your day-to-day work.

Reply to this Comment

I really love the way Angular handles scope automatically. I was kind of hoping for an unlink or teardown attribute in the directive though that would set the listener for me.

It goes without saying, you also need to do this for $interval.

Nice post, Thanks!

Reply to this Comment

Hi,
i am trying ng-switch to show a div and hide another on same condition.
in my controller i write the following code

$scope.currentView = 'view1';

$scope.loadView = function (viewName) {
$scope.currentView = 'view2';
};
My problem is after making the $scope.currentview as view2 the first statement is executed and it is again set to view 1 hence not changing the divs.My cshtml is

<div data-ng-switch on="currentView">
<div class="displayContents" data-ng-switch-when="view1">
</div>
</div>

<div class="createContent" data-ng-switch-when="view2">
</div>
please suggest me a solution
Thanks.

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.