Angular 2/4 handling errors in resolver

Anton Kononenko
by: Anton Kononenko | July 11, 2017

Angular 2/4 (or just Angular) is one of the most modern frameworks for the web-applications, which provides a lot of features, like templating, dependency injections, data querying, routing and much more.

Angular has great documentation with a lot of examples how to use all those features, but sometimes there not enough information, or not everything can be covered. Today I want to talk about Angular routing resolver.

Hope, readers aware about what is it, but if it’s not let me give a little explanation. Sometimes, we want to preload data before navigating to route, it might be lessons list, user personal data or whatever else. For such things, we can use resolvers. From the technical point of view, it’s service which implements interface Resolve and specified in route configuration. Usually, this data requested from some remote service, and all we know, that sometimes it might go wrong.

So let’s look at most typical resolver definition:

import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';
import { Observable } from 'rxjs/Observable';

import { LessonsService, Lesson } from 'lessons.service';

type Lessons = Array<Lesson>

@Injectable()
export class LessonsResolve implements Resolve<Lessons> {
    constructor(
       private lessonsService: LessonsService
    ) { }

    resolve(): Observable<Lessons> {
        return this.lessonsService.getLessons();
    }
}

Hope in this example would be everything clear. We have resolver which would return lessons from the service, and Angular would pass it to a component. Logic quite straight forward.

One thing which is not covered in this code example, that’s error handling. As we see from resolve signature this.lessonsService.getLessons() returns an observable, hopefully with data. But what if underlying service would fail and wouldn’t be able to return lessons? Well, we would expect that ErrorObservable would be returned. For service it’s totally fine, but what about our resolver? If resolver would return ErrorObservable it would break Angular, and there no right error handling for our actual users.

One of the solutions would be using Observable.prototype.catch method which actually designed for error handling:

    resolve(): Observable<Lessons> {
    return this.lessonsService.getLessons()
        .catch((err: any) => Observable.of([]));

Ok, that’s much better, at least we just avoiding of broken application, but let’s put us into users shoes. The user still expects to get his lessons, but instead of it, he sees nothing. Doesn’t sounds good. Can we determine in a component that resolver has failed? Nope, we can’t, as there actually might be none lessons. The only way out pass error to a component. Let’s do it:

    resolve(): Observable<Lessons> {
    return this.lessonsService.getLessons()
        .catch((err: any) => Observable.of(err));
    }

Ok, now in the case of error we propagate it to our component, and we can show an appropriate message for our user, that something went wrong, and they might contact our customer support team, or maybe they should try to refresh the page, or something else. From UX this is enough, but from the technical point of view, it’s not the best solution. We mixed 2 values into 1, cause it might be resolved to error or to actual data, also one more thing, as JS allow to throw any data, maybe error would be thrown as an empty array? Well, such things shouldn’t happen of course, but maybe this service is developed by another team, and they get to the idea that this is appropriate handling, as still, in the end, it would be thrown.

So let’s seek a better solution.

One of the option does not return actual lessons, but a wrapper. For wrapper, we can use new observable, promise, array, object or maybe something else. Let’s try to use an object. First of all, we need to define a new class for this:

class ResolvedValue<T> {
    constructor(
        public value: T,
        public error: Error = null
    ) {}

    hasError(): boolean {
        return error !== null;
    }
}

This quite simple wrapper would help us to distinguish behaviour for cases when an error happens and when not without playing with resolved type in component. Let’s also create some helpers function for wrapping actual response from service:

class UnclassifiedError extends Error {
    constructor(public originalError: any) {
        super();
    }
}

function wrapError(err: any): Error {
    return err instanceof Error ? err : new UnclassifiedError(err);
}

function handleResolveData<T>(observable: Observable<T>, defaultValue: T = null): Observable<ResolvedValue<T>> {
    return observable
        .map((value: T) => new ResolvedValue(value))
        .catch((error: any) => Observable.of(new ResolvedValue(defaultValue, wrapError(error))));
}

Ok, so now we have type safe error handling in the resolver. As you see in this code example, new error class has been presented, so even if null would be thrown it wouldn’t break our logic, and we would still be able to distinguish errors from the result. Also as optional enhancement default value can be provided in case of error, which might help for further error handling in a component.

There is only one thing left with such code, that types for resolver look complex, which easily can be solved:

type ResolveValue<T> = Resolve<ResolvedValue<T>>;
type ResolveValueStream<T> = Observable<ResolvedValue<T>>;

This will add some sugar to our definitions, and make them more readable. Let’s check out the final resolver version:

import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/catch';

import { LessonsService, Lesson } from 'lessons.service';

type Lessons = Array<Lesson>;

class UnclassifiedError extends Error {
    constructor(public originalError: any) {
        super();
    }
}

class ResolvedValue<T> {
    constructor(
        public value: T,
        public error: Error = null
    ) {}

    hasError(): boolean {
       return error !== null;
    }
}

type ResolveValue<T> = Resolve<ResolvedValue<T>>;
type ResolveValueStream<T> = Observable<ResolvedValue<T>>;

function wrapError(err: any): Error {
    return err instanceof Error ? err : new UnclassifiedError(err);
}

function handleResolveData<T>(observable: Observable<T>, defaultValue: T = null): ResolveValueStream<T> {
    return observable
        .map((value: T) => new ResolvedValue(value))
        .catch((error: any) => Observable.of(new ResolvedValue(defaultValue, wrapError(error))));
}

@Injectable()
export class LessonsResolve implements ResolveValue<Lessons> {
    constructor(
        private lessonsService: LessonsService
    ) { }

    resolve(): ResolveValueStream<Lessons> {
        return handleResolveData(
            this.lessonsService.getLessons(),
            []);
        }
    }
}

Of course, helpers classes, functions and types might be and actually should be moved to some separate common module, so it could be easily reused across the whole project.

Hope this approach would help you to build reliable web-applications in Angular with providing the best UX for your users.

Anton Kononenko

Anton Kononenko

Frontend Developer who always seeks for the best solution, taking into account all perspectives. Anton's motto: "everything is possible".
Informacja dotycząca plików cookies

Informujemy, iż w celu optymalizacji treści dostępnych w naszym serwisie, dostosowania ich do Państwa indywidualnych potrzeb korzystamy z informacji zapisanych za pomocą plików cookies na urządzeniach końcowych użytkowników. Pliki cookies użytkownik może kontrolować za pomocą ustawień swojej przeglądarki internetowej. Dalsze korzystanie z naszego serwisu internetowego, bez zmiany ustawień przeglądarki internetowej oznacza, iż użytkownik akceptuje stosowanie plików cookies.

Akceptuję