13

I have an app with a service which wraps my API calls:

var ConcernService = {
    ...
    get: function (items_url, objId) {
        var defer = $q.defer();
        $http({method: 'GET', 
            url: api_url + items_url + objId}).
            success(function (data, status, headers, config) {
                defer.resolve(data);
            }).error(function (data, status, headers, config) {
                console.log('ConcernService.get status',status);
                defer.reject(status);
            });
        return defer.promise;
    },

and I'm using UI-Router to transition between states:

concernsApp

    .config( function ($stateProvider, $urlRouterProvider) {

        $urlRouterProvider.otherwise("/404/");


        $stateProvider.state('project', {
        url: '/project/:projectId/',
        resolve: {
            project: function ($stateParams, ConcernService) {
                return ConcernService.get('projects/', $stateParams.projectId);
            },
        },
        views: {
            ...
        }
    });

I'm moving from using the normal AngularJS router and I'm having difficulty understanding how to implement 404s. I can see the ConcernService throwing the console.log status as rejected, but how do I catch this in the state router?

halfer
  • 19,824
  • 17
  • 99
  • 186
Darwin Tech
  • 18,449
  • 38
  • 112
  • 187

6 Answers6

22

The otherwise() rule is only invoked when no other route matches. What you really want is to intercept the $stateChangeError event, which is what gets fired when something goes wrong in a state transition (for example, a resolve failing). You can read more about that in the state change event docs.

The simplest implementation for what you're trying to do would be something like this:

$rootScope.$on('$stateChangeError', function(event) {
  $state.go('404');
});

Also, since $http itself is built on promises (which resolve resolves), your ConcernService method can be simplified down to a one-liner (I realize you expanded it for debugging purposes, but you could have just as easily chained it, just FYI):

var ConcernService = {

  get: function (items_url, objId) {
    return $http.get(api_url + items_url + objId);
  }
}
Nate Abele
  • 5,771
  • 32
  • 27
  • Right, dealing with the 404 like that totally makes sense. With the $http, though, if I implement it like you have if won't work as `objId `comes from the `$stateParams` and isn't yet resolved. Isn't this what the `defer` method is for? – Darwin Tech Apr 25 '14 at 17:08
  • I'm not really sure what you mean, but when you inject `$stateParams` into a resolve or `onEnter` function, you get a local copy representing the parameters for the state being transitioned to. – Nate Abele Apr 26 '14 at 00:35
  • @NateAbele May I ask how you got $state injected onto the event-handler: $stateChangeError? As I've got error: 'undefined $state' from your example. – Roy Lee Sep 28 '14 at 07:42
  • 2
    @Roylee You inject it into the same function you inject `$rootScope`. For example: `yourModuleName.run(function($rootScope, $state) { /* ... */ });` – Nate Abele Sep 29 '14 at 18:06
  • Looks reasonable to use $stateChangeError for this purpose. But how to know the code of the error? – Eugene Gluhotorenko Apr 27 '15 at 09:32
  • That's up to your application to decide. You can introspect the other parameters and figure out what kind of error state you want to transition to. – Nate Abele Apr 27 '15 at 17:29
  • Wouldn't something like that be more suitable for a 500 error? The stateChangeError is fired when some promise fails to resolve, and it's likely that the connection dropped or that the server stopped answering. – XelharK Jun 04 '15 at 13:10
  • 2
    @XelharK Depends on your use case. Perhaps a user followed a link from which a parameter is extracted and injected into an API call, which returns a 404. In this case, a "not found" error would be totally appropriate to show (however, it's kind of a moot point, since HTTP error codes/messages are totally orthogonal to SPAs). – Nate Abele Jun 04 '15 at 15:57
  • As of `v0.3.0` you can use `$rootScope.$on('$stateNotFound', () => $state.go('404'));` – Ian Clark Jul 12 '16 at 13:30
8

I differ between two 404 states:

Server:

  • show 404 page depending on server response HTTP Code 404
  • important to define no URL, so that user stays on URL where the error happened

Client:

  • URL is not found by angular ui router (none of defined URLs)

Code for Angular UI-Router state:

$stateProvider
  .state('404server', {
    templateUrl: '/views/layouts/404.html'
  })
  .state('404client', {
    url: '*path',
    templateUrl: '/views/layouts/404.html'
  });

Code in $httpProvider interceptor:

if(response.status === 404) {
  $injector.get('$state').go('404server');
}

And why I used $injector instead of $state is explained here.

Community
  • 1
  • 1
Betty St
  • 2,741
  • 21
  • 33
  • 1
    You should abort current transition `$state.transition.abort()`. Otherwise, you'll see `Transition Rejection The transition has been superseded by a different transition` – sad comrade Apr 03 '18 at 10:33
3

You can also try something like this and see if it works for you. You may need to adjust to your needs:

.state('otherwise', {
    abstract: true,
    templateUrl: 'views/404.html'
})
.state('otherwise.404', {
    url: '*path',
    templateUrl: 'views/404.html'
})
ucsarge
  • 1,260
  • 11
  • 10
  • 3
    I don't see why you need that abstract route, you can accomplish that with one state? – Betty St Oct 30 '14 at 14:12
  • It worked for me without the abstract route. I just have `.state('notFound' { url: '*path', templateUrl: 'views/404.html' })` – rjmunro Jul 21 '16 at 17:37
2

The $urlRouterProvider only works like a $watch to $location and if the actual URL matches one of the rules defined in the .config() function then it will redirect to the specified route.

Here's what I recommend, define "/404/" as a state:

$stateProvider.state('404', {
  url:'/404/',
  views:{
      ...
  }
});

And inside the reject() function move to 404 state

 if(status == '404'){
   $state.transitionTo('404');
 }

You will have to add ui-router as dependency of the project module and use the $state provider in your controller in order to be able to use $state.transitionTo()

Here's some info: https://github.com/angular-ui/ui-router/wiki/Quick-Reference#statetransitiontoto-toparams--options

Alejandro Figueroa
  • 419
  • 1
  • 4
  • 14
1

I managed to handle 404 without using $urlRoutProvider since I'm only using states by testing $state.transistion:

angular.module("app", []).run(["$state", "$rootScope", function($state, $rootScope) => {
    $rootScope.$on("$locationChangeSuccess", function() {
        if (!$state.transition) {
            $state.go("404");
        }
    });
}]);
Claudio Mezzasalma
  • 646
  • 1
  • 5
  • 23
0

$urlRouterProvider.otherwise('/page-not-found');

.state('error', {
  url: "/page-not-found",
  templateUrl: "templates/error.html",
  controller: "errorController"
})

Will handle your page not found problem.

If you want to raise 404 found purposefully use the state or url. We have created a separate controller just if you want to perform any operations.

Aurelio
  • 24,702
  • 9
  • 60
  • 63