AngularJS - Resolve $routeChangeError

The initial setup:

<div>
  <ng-view></ng-view>
</div>
var app = angular.module('app', []);

app.config(function ($routeProvider) {
  $routeProvider
    .when('/',
    {
      templateUrl: "app.html",
      controller: "AppCtrl"
      resolve: {
        loadData: appCtrl.loadData
      }
    }
  )
});

var appCtrl = app.controller("AppCtrl", function ($scope) {
  $scope.model = {
    message: "I'm a great app!"
  }
});

appCtrl.loadData = function ($q, $timeout) {
  var defer = $q.defer();
  $timeout(function () {
    defer.resolve("loadData"); 
  }, 2000);
  return defer.promise;
};
<h1>{{ model.message }}</h1>

The existing structure isn’t able to handle the scenario where the data fails to load, as demonstrated:

appCtrl.loadData = function ($q, $timeout) {
  var defer = $q.defer();
  $timeout(function () {
    defer.reject("loadData"); 
  }, 2000);
  return defer.promise;
};

Here, rejecting instead of resolving will cause the promise to fail. This, in turn, will cause the controller to never be instantiated and the view will never load.

To handle this failure, we retool the application to use appCtrl to manage route failures, and the viewCtrl to handlle all business logic in the view, which in this context is simply setting scope data.

The retooled application:

<div ng-app="app" ng-controlller="AppCtrl">
  <ng-view></ng-view>
</div>
var app = angular.module(‘app’, []);

app.config(function ($routeProvider) {
  $routeProvider
    .when('/',
    {
      templateUrl: "app.html",
      controller: "ViewCtrl"
      resolve: {
        loadData: viewCtrl.loadData
      }
    }
  )
});

app.controller("AppCtrl", function ($rootScope) {
  $rootScope.$on("$routeChangeError", function () {
    console.log("failed to change routes");
  });
});

var viewCtrl = app.controller("ViewCtrl", function ($scope) {
  $scope.model = {
    message: "I'm a great app!"
  }
});

viewCtrl.loadData = function ($q, $timeout) {
  var defer = $q.defer();
  $timeout(function () {
    defer.reject("loadData"); 
  }, 2000);
  return defer.promise;
};
<h1>{{ model.message }}</h1>

Focusing on the retooled AppCtrl:

    app.controller("AppCtrl", function ($rootScope) {
      $rootScope.$on("$routeChangeError", function () {
        console.log("failed to change routes");
      });
    });

Now, instead of having the AppCtrl handling the scope, it now handles what happens when route changes fail. It takes $rootScope as an argument to listen for events that come up to the root scope, and the $on method invoked on $rootScope is targeted at the $routeChangeError event, which we are intentionally throwing in the loadData() method. The second argument to $on is the callback function on the event.

The utility of this is that we can gracefully handle things like an HTTP request that fails to load.

The event callback also has 4 optional arguments: event, current, previous, and rejection:

    app.controller("AppCtrl", function ($rootScope) {
      $rootScope.$on("$routeChangeError", 
                     function (event, current, previous, rejection) {
        console.log("failed to change routes");
      });
    });

‘event’ is an object describing the environment surrounding the fired event, ‘current’ is the current route, ‘previous’ is the route that existed before this one was arrived at, and ‘rejection’ is a custom rejection message, which here is coming from the defer.reject() parameter.