Use the $destroy event to solve memory leaks



TL;DR: When your controller is destroyed, it emits $destroy. You can listen to this event and unbind variables, remove listeners, etc. to avoid leakage or undesired side effects. Demo time.

Angular is a convenient framework. Convenient, but also complex.
One of its main features are controllers, which make it easy to connect your model to your view. As your app grows, so does the number of controllers in it, which are instantiated and destroyed all the time. During that process, it is easy to introduce leaks by not cleaning up after your controllers. For example, if you have been using events on angular, you probably emit events using emit and listen to events using on. This works great until you create a listener for a classic event, using addEventListener. A classic event listener will not be automatically destroyed by angular, neither will your controller attached to it. This will introduce memory leaks and weird bugs.

Luckily, when a controller is destroyed it will emit the $destroy event, which you can listen to inside your controller by using:

myApp.controller('myController', function($scope) {

  ... // Your controller code goes here

  $scope.$on('$destroy', function() {
      // Do your clean up

This is the point in your controller where you can delete data that might otherwise stay binded (binded data won't be reclaimed by the garbage collector), clean up DOM event listeners, etc.

So, now that you know what $destroy is all about, let's try it out.

I set up a code pen where you can create or destroy a controller by clicking in the button Click to toggle controller. This creates a new controller that listens for a dispatched event and a button that emits said event. The listener logs when the controller was instantiated.

Since we are using $scope.$emit and $scope.$on the controller cleans up after itself. To verify this, do the following:

  • create a controller with the toggle button
  • dispatch an event
  • see the date in the console log
  • Destroy the controller with the toggle button
  • Repeat

This will give you two different dates for the two times the controller was instantiated.

Unfortunately, these functions only work for events dispatched in angular, so if you had an event emitted using element.dispatchEvent from another library (a chart, a react component; etc.) you couldn't use $scope.$on to listen to it.

A first approach would be to replace the $scope.$on, with a document.addEventListener. This, however, will create a new listener in the document element every time you create a new controller.

If you test the buttons now, you will see that we log once for every controller that was instantiated, so even though the controller was destroyed, the eventListener attached to the document element is still there.

To fix this, enter the $destroy event. All you have to do is listen to it using $scope.$on and use document.removeEventListener to remove the listener. Just add this to your controller:

$scope.$on('$destroy', function() {
  console.log('clean up');
  // Remove the listener
  document.removeEventListener('dispatched', listener);

If you now try the buttons, it should all work as expected.