I'm trying to use resolvers in order to make a better UX. Everything works great on the happy path. What I can't seem to figure out is how to handle exceptions. My resolver calls a service, which hits a webapi project. An example:
FooResolver:
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Foo> { return this.fooService.getById(route.params['id']).catch(err => { ****not sure what to do/return in the case of a server error**** return Observable.throw(err); }); }
FooService:
public getById(id: string): Observable<Foo> { return this.http.get(`${ this.apiUrl }/${ id }`) .map(this.extractData) .catch(this.handleError); }
The handleError function:
protected handleError (error: Response | any) { // Todo: Log the error // Errors will be handled uniquely by the component that triggered them return Observable.throw(error); }
Inside the FooComponent, I do this (this is never hit in the event of an error returned from the service/resolver):
FooComponent:
ngOnInit(): void { this.foo= this.route.snapshot.data['foo']; if (this.foo) { this.createForm(this.foo); } }
I've tried throwing the error (as shown) - I get this exception in the console:
Uncaught (in promise): Response with status: 500 Internal Server Error for URL:
and returning new Observable<Foo>()
, which gives:
Cannot read property 'subscribe' of undefined
I have a few resolvers, all of which can experience exceptions on the server, But I don't know what to do in the event of these exceptions.
You can specify an error code and error data for the error. Consider specifying the error data to catch specific errors. For example, you could filter on the error code for the types of errors that are caught and map the error code to a variable after the errors are caught.
The Shortcomings of Angular's ErrorHandler One traditional way of handling errors in Angular is to provide an ErrorHandler class. This class can be extended to create your own global error handler. This is also a useful way to handle all errors that occur, but is mostly useful for tracking error logs.
To test the resolver we need to render RouterOutlet . const fixture = MockRender(RouterOutlet); Additionally, we also need to properly customize mocked services if the resolver is using them to fetch data. const dataService = TestBed.
Here is an example of one of my resolvers with error handling, using the technique that Gunter suggests:
import { Injectable } from '@angular/core'; import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/map'; import 'rxjs/add/observable/of'; import { IProduct } from './product'; import { ProductService } from './product.service'; @Injectable() export class ProductResolver implements Resolve<IProduct> { constructor(private productService: ProductService, private router: Router) { } resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<IProduct> { let id = route.params['id']; if (isNaN(+id)) { console.log(`Product id was not a number: ${id}`); this.router.navigate(['/products']); return Observable.of(null); } return this.productService.getProduct(+id) .map(product => { if (product) { return product; } console.log(`Product was not found: ${id}`); this.router.navigate(['/products']); return null; }) .catch(error => { console.log(`Retrieval error: ${error}`); this.router.navigate(['/products']); return Observable.of(null); }); } }
You can find the complete example here: https://github.com/DeborahK/Angular-Routing in the APM-final folder.
UPDATE Feb 2019
Here is a better answer for error handling in a resolver:
/* Defines the product entity */ export interface Product { id: number; productName: string; productCode: string; category: string; tags?: string[]; releaseDate: string; price: number; description: string; starRating: number; imageUrl: string; } export interface ProductResolved { product: Product; error?: any; }
import { Injectable } from '@angular/core'; import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { Observable, of } from 'rxjs'; import { map, catchError } from 'rxjs/operators'; import { ProductResolved } from './product'; import { ProductService } from './product.service'; @Injectable({ providedIn: 'root', }) export class ProductResolver implements Resolve<ProductResolved> { constructor(private productService: ProductService) {} resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<ProductResolved> { const id = route.paramMap.get('id'); if (isNaN(+id)) { const message = `Product id was not a number: ${id}`; console.error(message); return of({ product: null, error: message }); } return this.productService.getProduct(+id).pipe( map((product) => ({ product: product })), catchError((error) => { const message = `Retrieval error: ${error}`; console.error(message); return of({ product: null, error: message }); }), ); } }
ngOnInit(): void { const resolvedData: ProductResolved = this.route.snapshot.data['resolvedData']; this.errorMessage = resolvedData.error; this.product = resolvedData.product; }
You need to return an observable that completes with false
handleError() { return Observable.of([false]); }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With