How to handle error in a Resolver

AngularRxjs

Angular Problem Overview


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.

Angular Solutions


Solution 1 - Angular

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:

  1. Wrap your interface in another interface with an optional error property:
/* 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;
}
  1. Resolve to that interface:
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 });
      }),
    );
  }
}
  1. In the component, pull off the piece of the interface you need:
ngOnInit(): void {
  const resolvedData: ProductResolved = this.route.snapshot.data['resolvedData'];
  this.errorMessage = resolvedData.error;
  this.product = resolvedData.product;
}

Solution 2 - Angular

You need to return an observable that completes with false

handleError() {
  return Observable.of([false]);
}

Solution 3 - Angular

I only wanted to provide a very similar updated answer but with some updated code. I personally think the error should simply not be managed inside the resolver.

Why? Well, I think that the resolver's job is to resolve, not to figure out what to do if it can't resolve. We might be using this resolver in two or three different components, and each of them might decide to react in a different way if the resolver fails. We might want to redirect in one of them to a 404 page, but in other maybe we just try to fix graciously the error by displaying something else.

Sure, we might also want to react differently depending on what error we get: maybe the user was not authorised, or maybe that item was deleted or didn't even exist on the first place, who knows. We might want to display different results and in that case I totally upvote DeborahK's updated answer. But for most cases, I think that overcomplicates the matter (an extra interface only for the resolver, making sure the error inside it's descriptive...) and that we probably won't really care why the resolver failed: it just did, let the component that needed that item figure out what to do, and move on.

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/internal/operators';
import { Product } from '../_interfaces';
import { ProductsService } from '../_services';

@Injectable()
export class ProductResolver implements Resolve<Product> {

	constructor(private productsService: ProductsService) {
	}

	public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> {
		const id = route.paramMap.get('id');
		return this.productsService.getProduct(id).pipe(
			catchError(error => {
				console.error(`Can't resolve product with id ${id} because of the error:`);
				console.error(error);
				return of(null);
			})
		);
	}
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMatt MView Question on Stackoverflow
Solution 1 - AngularDeborahKView Answer on Stackoverflow
Solution 2 - AngularGünter ZöchbauerView Answer on Stackoverflow
Solution 3 - AngularRTYXView Answer on Stackoverflow