How to handle error in angular-ui-router's resolve
Asked Answered
F

2

43

I am using angular-ui-router's resolve to get data from server before moving to a state. Sometimes the request to the server fails and I need to inform the user about the failure. If I call the server from the controller, I can put then and call my notification service in it in case the call fails. I put the call to the server in resolve because I want descendant states to wait for the result from the server before they start.

Where can I catch the error in case the call to the server fails? (I have read the documentation but still unsure how. Also, I'm looking for a reason to try out this new snippet tool :).

"use strict";

angular.module('MyApp', ["ui.router"]).config([
  "$stateProvider",
  "$urlRouterProvider",
  function ($stateProvider, $urlRouterProvider) {
    $urlRouterProvider.otherwise("/item");
    $stateProvider
    .state("list", {
      url: "/item",
      template: '<div>{{listvm}}</div>' +
      	'<a ui-sref="list.detail({id:8})">go to child state and trigger resolve</a>' +
        '<ui-view />',
      controller: ["$scope", "$state", function($scope, $state){
          $scope.listvm = { state: $state.current.name };
      }]
    })
    .state("list.detail", {
      url: "/{id}",
      template: '<div>{{detailvm}}</div>',
      resolve: {
        data: ["$q", "$timeout", function ($q, $timeout) {
          var deferred = $q.defer();
          $timeout(function () {
            //deferred.resolve("successful");
            deferred.reject("fail");   // resolve fails here
          }, 2000);
          return deferred.promise;
        }]
      },
      controller: ["$scope", "data", "$state", function ($scope, data, $state) {
        $scope.detailvm = {
          state: $state.current.name,
          data: data
        };
      }]
    });
  }
]);
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.22/angular.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.10/angular-ui-router.min.js"></script>

<div ng-app="MyApp">
  <ui-view />
</div>
Famous answered 21/9, 2014 at 18:21 Comment(0)
K
22

The issue is that if any of the dependencies in the route resolve is rejected, the controller will not be instantiated. So you could convert the failure to data that you can detect in the instantiated controller.

Example Pseudocode:-

   data: ["$q", "$timeout","$http", function ($q, $timeout, $http) {
      return $timeout(function () { //timeout already returns a promise
        //return "Yes";
        //return success of failure
         return success ? {status:true, data:data} : {status:false}; //return a status from here
       }, 2000);
     }]

and in your controller:-

 controller: ["$scope", "data", "$state", function ($scope, data, $state) {
      //If it has failed
      if(!data.status){
        $scope.error = "Some error";
       return;
      }
        $scope.detailvm = {
          state: $state.current.name,
          data: data
        };

If you are making an $http call or similar you can make use of http promise to resolve the data always even in case of failure and return a status to the controller.

Example:-

resolve: {
        data: ["$q", "$timeout","$http", function ($q, $timeout, $http) {
           return $http.get("someurl")
             .then(function(){ return {status:true , data: "Yes"} }, 
                    function(){ return {status:false} }); //In case of failure catch it and return a valid data inorder for the controller to get instantated
        }]
      },

"use strict";

angular.module('MyApp', ["ui.router"]).config([
  "$stateProvider",
  "$urlRouterProvider",
  function ($stateProvider, $urlRouterProvider) {
    $urlRouterProvider.otherwise("/item");
    $stateProvider
    .state("list", {
      url: "/item",
      template: '<div>{{error}}</div><div>{{listvm}}</div>' +
      	'<a ui-sref="list.detail({id:8})">go to child state and trigger resolve</a>' +
        '<ui-view />',
      controller: ["$scope", "$state", function($scope, $state){
       $scope.listvm = { state: $state.current.name };
      }]
    })
    .state("list.detail", {
      url: "/{id}",
      template: '<div>{{detailvm}}</div>',
      resolve: {
        data: ["$q", "$timeout","$http", function ($q, $timeout, $http) {
           return $http.get("/").then(function(){ return {status:true , data: "Yes"} }, function(){ return {status:false} })
        }]
      },
      controller: ["$scope", "data", "$state", function ($scope, data, $state) {
   
    
        $scope.detailvm = {
          state: $state.current.name,
          data: data.status ? data :"OOPS Error"
        };
        
      }]
    });
  }
]);
 <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.22/angular.min.js"></script>
    <script data-require="angular-ui-router@*" data-semver="0.2.10" src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.10/angular-ui-router.js"></script>
  <div ng-app="MyApp">
      <ui-view></ui-view>
    </div>
Kaczmarek answered 21/9, 2014 at 19:19 Comment(1)
This way you have to implement error handling in each controller, right? I like better the approach where you listen for error event that is broadcasted by ui router and respond to that, since error handling is centralized in that case.Beamon
S
82

Old question but I had the same problem and stumbled on this in ui-router's FAQ section

If you are having issues where a trivial error wasn't being caught because it was happening within the resolve function of a state, this is actually the intended behavior of promises per the spec.

errors within resolve.

So you can catch all resolve errors in the run phase of your app like this

$rootScope.$on('$stateChangeError', 
function(event, toState, toParams, fromState, fromParams, error){ 
        // this is required if you want to prevent the $UrlRouter reverting the URL to the previous valid location
        event.preventDefault();
        ... 
})
Sizeable answered 7/10, 2015 at 9:24 Comment(1)
This should be the accepted answer. imho the whole point of using resolve is to not have to deal with data in the controller!Ioved
K
22

The issue is that if any of the dependencies in the route resolve is rejected, the controller will not be instantiated. So you could convert the failure to data that you can detect in the instantiated controller.

Example Pseudocode:-

   data: ["$q", "$timeout","$http", function ($q, $timeout, $http) {
      return $timeout(function () { //timeout already returns a promise
        //return "Yes";
        //return success of failure
         return success ? {status:true, data:data} : {status:false}; //return a status from here
       }, 2000);
     }]

and in your controller:-

 controller: ["$scope", "data", "$state", function ($scope, data, $state) {
      //If it has failed
      if(!data.status){
        $scope.error = "Some error";
       return;
      }
        $scope.detailvm = {
          state: $state.current.name,
          data: data
        };

If you are making an $http call or similar you can make use of http promise to resolve the data always even in case of failure and return a status to the controller.

Example:-

resolve: {
        data: ["$q", "$timeout","$http", function ($q, $timeout, $http) {
           return $http.get("someurl")
             .then(function(){ return {status:true , data: "Yes"} }, 
                    function(){ return {status:false} }); //In case of failure catch it and return a valid data inorder for the controller to get instantated
        }]
      },

"use strict";

angular.module('MyApp', ["ui.router"]).config([
  "$stateProvider",
  "$urlRouterProvider",
  function ($stateProvider, $urlRouterProvider) {
    $urlRouterProvider.otherwise("/item");
    $stateProvider
    .state("list", {
      url: "/item",
      template: '<div>{{error}}</div><div>{{listvm}}</div>' +
      	'<a ui-sref="list.detail({id:8})">go to child state and trigger resolve</a>' +
        '<ui-view />',
      controller: ["$scope", "$state", function($scope, $state){
       $scope.listvm = { state: $state.current.name };
      }]
    })
    .state("list.detail", {
      url: "/{id}",
      template: '<div>{{detailvm}}</div>',
      resolve: {
        data: ["$q", "$timeout","$http", function ($q, $timeout, $http) {
           return $http.get("/").then(function(){ return {status:true , data: "Yes"} }, function(){ return {status:false} })
        }]
      },
      controller: ["$scope", "data", "$state", function ($scope, data, $state) {
   
    
        $scope.detailvm = {
          state: $state.current.name,
          data: data.status ? data :"OOPS Error"
        };
        
      }]
    });
  }
]);
 <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.22/angular.min.js"></script>
    <script data-require="angular-ui-router@*" data-semver="0.2.10" src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.10/angular-ui-router.js"></script>
  <div ng-app="MyApp">
      <ui-view></ui-view>
    </div>
Kaczmarek answered 21/9, 2014 at 19:19 Comment(1)
This way you have to implement error handling in each controller, right? I like better the approach where you listen for error event that is broadcasted by ui router and respond to that, since error handling is centralized in that case.Beamon

© 2022 - 2024 — McMap. All rights reserved.