angular: Exception/Error In Resolvers (Router) can not be catched in the subscription to "activatedRoute.data"

I’m submitting a … (check one with “x”)

[X ] bug report => search github for a similar issue or PR before submitting
[ ] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

I’m using the Router with Resolver interfaces to get some data form a service (backend) before creating a component which will render the data (subscribing to it). If an exception occurs in the Resolver the application breaks. (Error: Uncaught (in promise))

Expected behavior

If there is an exception in the Resolver i expect the subscribe error function to be called but the ErrorObservable appears in the “activatedRoute.data.value” instead.

image

// some component
    ngOnInit() {
        this.activatedRoute.data
            .subscribe(
                (data) => {
                    this.data = data.data] || {};
                    console.log('data was set to ' + this.data)
                },
                (error) => {
                    /// we never get here, no matter there was a Promise.reject(), throw Error(), etc.
                     console.log('ups! Error getting data')
                     alert(error);
                });
    }

// some resolver
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BehaviorSubject<any>|Observable<any>|Promise<any>|any {
     return Observable.throw('This always fails');
}

Minimal reproduction of the problem with instructions

check out the plunker http://plnkr.co/edit/MaiM6T1COr2ppB2AMANx

What is the motivation / use case for changing the behavior?

Catch and treat exceptions within the target component Please tell us about your environment:

  • Angular version: 2.0.X 2.3.1

Yes

  • Browser: [all | Chrome XX | Firefox XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ]

All

  • Language: [all | TypeScript X.X | ES6/7 | ES5] TypeScript 2.1.1
  • Node (for AoT issues):node --version =

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 28
  • Comments: 29 (5 by maintainers)

Most upvoted comments

I totally agree with @OscarGalindo . The problem here is that you can not take over the router error handling (or the resolver’s). If the the resolver fails, the router simply does not do the navigation. So from my modest point of view we have 4 options here:

  1. Do nothing. Cons: This is a workaround an it is not intuitive.
  2. Let the component decide how to deal with the error. Cons: I don’t see any. You always get to the component no matter what and treat the error there.
  3. Import the router in the Resolver and if there is an exception then navigate to whatever url. Cons: I haven’t tried it so don’t know if it works. Routes are dispersed within the code.
  4. Add another attribute ‘errorComponent’ to the ResolveData type as follows so we can regain control over the error. Cons: You need another component… more boilerplate code.

{ path: 'products/:id', component: ProductComponent, resolve: { data: ProductService errorComponent: ProductErrorComponent }, }

Honestly, I would go for solution 2 which was the initial idea and the most intuitive one.

interface ResolvedData {
  data?: any;
  error?: any;
}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<ResolvedData> {
    return this.http.get(url)
         .map(r => {data: r.json()})
         .catch(e => Observable.of({error: e});
  }

is more flexible solution as to me, than 1 time observable

I’ll be completely honest, I thought about this for a while, and did some preliminary research into what would need to be done to fix it, and I’m beginning to disagree that this is a bug, and that @DzmitryShylovich’s workaround is actually the proper solution.

The purpose of a resolver is to resolve critical data that must exist before the route even loads. If you’re dealing with data that does not have to be resolved pre-activation, then you shouldn’t use a resolver anyway, and just load the data in ngOnInit(). Therefore, it wouldn’t make sense to continue with a successful navigation if a resolver fails with an error, because that would mean that critical data required for the route failed to load. Attempting to catch the error from the resolver is conflating a failure in navigation with a failure in application-specific business logic. If a component expects to successfully load and receive information regarding a failed resolver, then that is still a navigation success, albeit an application failure. Therefore, it is the resolver’s responsibility to catch any application errors, and return them as a navigation success in a data structure that properly communicates the state of the resolver to the component.

I think the actual bug here is that ActivatedRoute.data is an Observable in the first place, if it is already resolved when the component is loaded. But this is really just misleading rather than a functional bug.

I used the Resolve example from thoughtram and modified it so that it would break in the route.
https://plnkr.co/edit/wLosw5 That is, ContactResolve will call ContactsService.failGetContact which performs Observable.throw('...'); When this happens, the exception is logged on the console and the route stays on ContactsListComponent.

I want to handle this error at the component level. But the ContactsDetailComponent is never loaded. Should I be handling the error in the ContactsListComponent or ContactsDetailComponent ? If on CLC, how?

@Parent5446

I’ll be completely honest, I thought about this for a while, and did some preliminary research into what would need to be done to fix it, and I’m beginning to disagree that this is a bug, and that @DzmitryShylovich’s workaround is actually the proper solution.

I will agree that your logic is sound regarding the reason for a resolver guard in the first place (must resolve data before a route can be resolved) but I disagree that this isn’t a fundamental bug for a number of reasons:

  1. The type changes depending on result; an error will return a catchable Observable return but a success will result in an Object. You can’t subscribe to both as only the error returns the Observable.
  2. to @polyglotinc’s point there seems to be no official Angular stance on error handling in a resolver, or more importantly, to @KostyaTretyak’s point, we don’t have a error handler option on the router if the resolver fails; I think you are both right, it shouldn’t be an Observable in the first place but we should have a proper error handler.
  3. @DzmitryShylovich’s is not a proper solution because you don’t have any context for the error; for example: did this happen on x component or y component. @DzmitryShylovich’s solution only encourages you to handle the error on the resolved component anyways (which goes against to what you’re saying about a making sure the data is resolved before you route)

While I realize there are workarounds for all these things it is unnecessarily cumbersome and arguably goes against the ES standards because we are expected to believe that a Promise in = Promise out regardless of level.

Somewhat off topic, but, if anything, in my opinion, the Router in Angular needs a complete re-write. It’s easily the worst part.

I think that we can pass an error from a resolver to a component right now. See my example on stackblitz:

resolve() {
  // Simulating some error
  const mockError = {status: 404, error: 'some error message here'};
  return throwError(new HttpErrorResponse(mockError)).pipe(
    // Here you catch error and just pass it to AboutComponent
    catchError(error => of(error))
  );
}

But it would be nice if the resolver could accept some thing like this:

{
  path: '/route-path',
  component: SomeComponent,
  resolve: {
    data: {
      success: SomeResolverService,
      error: HandleErrorComponent
  }
}

or just:

{
  path: '/route-path',
  component: SomeComponent,
  errorComponent: HandleErrorComponent
  resolve: {
    data: SomeResolverService
  }
}

And if SomeResolverService fail, then user can go to /route-path, but it will see HandleErrorComponent

Same problem here using 2.4.2.

I did a “catch” in the resolve which returns an empty observable and check inside component if empty or not.