18

Say I have a ValuesComponent that displays an array of Value in an HTML table.

// in value.ts export class Value { ... } 
// in values.component.ts @Component(...) export class ValuesComponent { ... } 

Being a good programmer and all, I've created a different class that is responsible for providing values. Let's call it the ValuesService.

// in values.service.ts @Injectable() export class ValuesService { public getValues(): Observable<Value[]> { ... } } 

Suppose that the service gets its values from a web service: /api/values

Now instead of injecting the service directly into my component, I want to let the Angular router pre-fetch the values before navigating to the component.

For that, I created a Resolve service class and plugged it into the router module.

// in values-resolver.service.ts export class ValuesResolverService implements Resolve<Value[]> { constructor(private backend: ValuesService) { } public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Value[]> { return this.backend.getValues(); } } 
// In app.module.ts (in @NgModule()) imports: [ RouterModule.forRoot([{ path: 'values', component: ValuesComponent, resolve: { values: ValuesResolverService } }]) ] 
// In values.component.ts public values: Value[]; constructor(private route: ActivatedRoute) { } ngOnInit() { this.values = route.data.values as Value[]; } 

How am I doing so far? Good? Then where do I put my error handling for when ValuesService.getValues() fails? (connection error, internal server error, ...)

When a user tries to navigate to the /#/values route and an error occurs, I want to log the error to the console and stop navigation. Ideally, I'd like to redirect the user to the /#/error route (not shown in the example code).

16
  • The question doesn't mention what exactly should happen on error. Commented Nov 8, 2016 at 14:54
  • A simple console.log is fine. I just need to know where to put it and what to return (if anything). Commented Nov 8, 2016 at 14:58
  • Then the question is about observables, not resolvers. It's return this.backend.getValues().catch(...). Commented Nov 8, 2016 at 15:01
  • 1
    Obviously, you have probably thought of this but you probably need to return a Promise, an Observable or some other monadic abstraction from the resolver instead of the array of values itself. That way you can attach error handling inside the resolver and, by injecting the router into it as well, you can trigger navigation in .catch. Actually, it looks like you are returning an Observable<Value[]> so you can attach error handling in the call to ValuesService and then redirect in the catch block. I think this will work. Commented Feb 12, 2017 at 13:43
  • 2
    @AluanHaddad I did not think about that. You should convert your comment to an answer. Commented Feb 13, 2017 at 7:20

1 Answer 1

3

In such a situation, the Service should handle the error, and redirect to the wanted route. The catch method should then return an empty Observable (for the resolve guard to resolve), while handling empty cases during within the resolve.

Something like this

export class ValuesService { constructor(private http:Http, private router:Router) {} getValue() { this.http.get('/some/url').map((response:Response) => { // Map your response to a model here }).catch((response:Response) => { // Handler case for different status this.router.navigate(['/error']); return Observable.empty(); }) } } 

And the guard to be like the following

export class ValueResolveGuard implements Resolve<Value[]> { constructor(private valueService:ValueService) {} resolve() { // You don't need the route & state if you're not gonna use them this.valueService.getValue().toPromise().then((values:Values[]) => { if (!values) { // For the Observable.empty() case // return some default stuff } return values; }) } } 

Alternatively, if you want to specifically return an observable (shouldn't make any difference in this case, since the resolve guard, accepts both an observable and a promise), you can do something like this.

 this.valueService.getValue().first().catch({} => { // return some default value. }); 

Notice that first() is required, so that the observable returned would be a terminated observable, otherwise the resolve guard wouldn't resolve. The catch is needed because a first() call on an empty observable would throw an error.

Sign up to request clarification or add additional context in comments.

5 Comments

I disagree with letting the service handle navigation in general, but especially with navigation as a side effect of a failing getter. I prefer my getters to be free of side effects.
I think it all boils down to how the service is used. A service should be a point of common functionality. If it is expected that on error, the route should always be updated, then I do think it is the right place to do. In the case that the service is called multiple times, from different execution points, and require the same behavior, not placing the same routing within the service would result your code in not being DRY. You should ask the question, what should be the purpose of this service? That would help you identify the functionality that is required even better.
You could extract the catch function and put it in a separate file. Then it is easy to reuse anywhere: .getValues().catch(navigateToErrorPage). That's dry enough for my taste.
As much as I hate to admit it, this seems like the only way, so what you can do is create a general logger class that has a property observable, which is observed in an error UI component sitting somewhere and call the logger inside the catch block... I personally still use Javascript to inject UI messages, simpler, cleaner, more robust.
hmmm...doesnt resolve() need to return something? That return statement is async.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.