30

Inside my Angular application I handle routes/states via ui-router. If everything works - it is great. But what is a good way to handle errors occurring inside resolve functions?

My current solution: I have a to dedicated error state (similar to the common 404.html). Which looks like this:

// inside config()
.state('error', {
  url: '/error',
  controller: 'ErrorCtrl',
  templateUrl: 'error.html' // displays an error message
})

If an error occurs inside resolve I catch it via the broadcasted $stateChangeError in m run function:

angular.module('myModule').run(function($state) {
  $rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error) {
    event.preventDefault();
    $state.go('error');
  });
});

This works, but I want to change my error message inside my 'error.html' dependent on the error. I don't want to pollute the $rootScope and I want to do it in a ui-router'esk way.

My current solution uses $stateParams to the error data to my error state, but I have to use JSONified query params for this and get a very ugly URL:

// inside config()
.state('error', {
  url: '/error?data&status&config', // accept these three params
  controller: 'ErrorCtrl',
  templateUrl: 'error.html' // displays an error message
})

angular.module('myModule').run(function($state) {
  $rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error) {
    event.preventDefault();
    $state.go('error', JSON.stringify(error)); // error has data, status and config properties
  });
});

Question Is there a different way how I can pass the error object to my error state without uglifying my URL? (Note: My error object is complex and not just a simple string.)

Jens
  • 5,767
  • 5
  • 54
  • 69
Pipo
  • 5,623
  • 7
  • 36
  • 46

3 Answers3

27

A strategy that worked for me is to have the resolve functions return a rejected promise with the error object:

$stateProvider.state('needs_authentication', {
    url: '/auth',
    resolve: {
        user: function ($q, authService) {
            if (authService.isAuthenticated()) {
                ...
            }
            else {
                var errorObject = { code: 'NOT_AUTHENTICATED' };
                return $q.reject(errorObject);
            }
        }
    }
});

This lets your $stateChangeError function be able to handle specific error conditions:

$rootScope.$on('$stateChangeError', function (evt, toState, toParams, fromState, fromParams, error) {
    if (angular.isObject(error) && angular.isString(error.code)) {
        switch (error.code) {
            case 'NOT_AUTHENTICATED':
                // go to the login page
                $state.go('login');
                break;
            default:
                // set the error object on the error state and go there
                $state.get('error').error = error;
                $state.go('error');
        }
    }
    else {
        // unexpected error
        $state.go('error');
    }
});
Miller
  • 2,742
  • 22
  • 20
  • Much cleaner and idiomatic approach. – Richard Clayton Jul 16 '15 at 17:35
  • Where do you put $rootScope.$on? – Benny Powers Apr 05 '16 at 09:17
  • @Benny $rootScope.$on goes inside a run() function, for some more info see http://stackoverflow.com/questions/20663076/angularjs-app-run-documentation – Miller Apr 07 '16 at 00:40
  • @Miller perfect! Thanks for the tip this solved my problem http://stackoverflow.com/q/36427578/2515275 – Benny Powers Apr 07 '16 at 05:09
  • @Miller have been doing the exact same thing. Except I don't want url to change when there is an error, I just want the error page to show. I tried $state.go('error', null , {location: false}); which works if it is the first page load. But when you click on a link that takes you to this error state, it does not change url to target states url. Is there a way to do this ? – Bren May 24 '16 at 16:10
  • @gomyes my normal strategy for doing this is to have your error view outside of the angular-ui-router system, this might be a top level div that fills the screen and only becomes visible when an error occurs. So instead of $state.go() you might use $rootState.$broadcast() to broadcast an error event that causes the your error div/view to reveal itself – Miller May 26 '16 at 00:18
  • Clean and nice approach, but if you use $state.go() in $stateChangeError handler, you need to add event.preventDefault() to prevent ui-router from reverting the URL to the previous valid location. – BorisS Aug 23 '16 at 18:13
  • Not to be a nit-picker, just for good code cleanliness :) ; there should be a `break;` at the end of the default clause. But a very good clean solution. :) – Guy Park Aug 13 '18 at 13:34
26

You could pass data through a service, that you should access in your error controller or in onEnter method of error state.

Or you could "enrich" your error state. Maybe it's NOT the angular way, but what I mean is:

$rootScope.$on('$stateChangeError', function (event, toState, toParams, fromState, fromParams, error) {
  event.preventDefault();
  $state.get('error').error = { code: 123, description: 'Exception stack trace' }
  return $state.go('error');
});

And in your error state config:

.state('error', {
    url: 'error',
    resolve: {
        errorObj: [function () {
            return this.self.error;
        }]
    },
    controller: 'ErrorCtrl',
    templateUrl: 'error.html' // displays an error message
});

Hope this helps

wilver
  • 2,106
  • 1
  • 19
  • 26
4

You can remove the whole url: option and replace this with just a params: option.

                .state('error', {
                    abstract: false,
                    templateUrl: templateFor('error'),
                    controller: controllerFor('error'),
                    params: { 'error': 'An error has occurred' },
                    resolve: {
                        error: [
                            '$stateParams', function ($stateParams) {
                                return  $stateParams.error;
                            }
                        ]
                    }
                })

Now there is no ugly url. and the controller can get the error as a parameter.

Peter Marshall
  • 1,231
  • 1
  • 13
  • 22