How to handle error in a Resolver

Solution 1:

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:

You need to return an observable that completes with false

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

Solution 3:

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);
            })
        );
    }
}