Angular Error-Handling

Angular Error-Handling

Just simple and effective...

In this article I will show you how you can add a global error-handling to an Angular application.

The requirements for the error-handling:

  • should work with the Angular version > 13

  • error-handling should show an dedicated error-page, if an error occurs anywhere within the application

  • errors should be divided into two types: clientside-errors & serverside-errors

Table of contents

1. Step: Create an Angular ErrorHandling-Module
2. Step: Create the Error-Page
3. Step: Create Custom Error Handler
4. Step: Create Custom Error Type
5. Step: Create Testpage
6. Step: Create BackendService
7. Step: Create routing to the TestPage
8. Step: Create routing to the ErrorPage
9. Step: Create HttpInterceptor to catch the BackendErrors

## 1. Step: Create an Angular ErrorHandling-Module In order to separate all responsibilities regarding error handling to one place, we create an extra module for it. You can do this by typing following command:

ng g m errorHandling --routing --module App
  • ** Ensure your Angular ErrorHandlingModule is imported in the AppModule!**

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ErrorHandlingModule } from './error-handling/error-handling.module';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ErrorHandlingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

## 2. Step: Create the Error-Page

This page is used to show the error to the user.

You can use following command to create the error-page:

ng g c error-handling/pages/errorPage --skip-tests

Replace the error-page html with following html:

error-handling/pages/error-page/error-page.component.html

<div class="title">Error-Page</div>
<div class="details text-align-center">
    The application has no errors detected!
</div>


<div *ngIf="error" class="details">
    <pre>
            {{error | json }}
    </pre>
</div>
<button *ngIf="error" (click)="restartApp()">restart application</button>

Replace the error-page styles with following:

error-handling/pages/error-page/error-page.component.scss

.title {
  text-align: center;
  font-size: 2em;
  font-weight: bold;
  margin-bottom: 1em;
}

.details {
  width: 50%;
  overflow: auto;
  margin: auto;
}

.text-align-center {
  text-align: center;
}

button {
  display: block;
  margin: auto;
}

And finally replace the error-page component class with:

error-handling/pages/error-page/error-page.component.ts

import { Component } from '@angular/core';
import { Router } from '@angular/router';
// import { CustomError } from '../../custom-error';

@Component({
  selector: 'app-error-page',
  templateUrl: './error-page.component.html',
  styleUrls: ['./error-page.component.scss']
})
export class ErrorPageComponent {

  error?: any;

  constructor(private router: Router) {
    // beware to call getCurrentNavigation() within the constructor, everything later
    // is to late, because the navigation step will be executed and the state is lost.
    const state = this.router.getCurrentNavigation()?.extras?.state;
    if (state) {
      // this.error = state['lastError'] as CustomError;
    }
  }

  public restartApp(): void {
    window.location.href = '/';
  }

}

## 3. Step: Create Custom Error Handler The custom error handler uses Angular's DI technology to override the standard ErrorHandler.

error-handling/custom-error-handler.ts

import { HttpErrorResponse } from '@angular/common/http';
import { ErrorHandler, Injectable, Injector, NgZone } from '@angular/core';
import { Router } from '@angular/router';
// import { CustomError } from './custom-error';

@Injectable({
    providedIn: 'root'
})
export class CustomErrorHandler implements ErrorHandler {

    constructor(private injector: Injector, private ngZone: NgZone) {
    }

    handleError(error: any): void {
        try {
            const customError = this.parseError(error);
            const router = this.injector.get(Router);

            if (NgZone.isInAngularZone()) {
                router.navigate(['/error'], { state: { lastError: customError } });
            } else {
                this.ngZone.run(() => {
                    router.navigate(['/error'], { state: { lastError: customError } });
                });
            }

         console.error(error);

        } catch (error) {
            console.error('Error occured within custom-error-handler.ts', error);
        }
    }

    private parseError(error: any): any {
        let customError: any;

        if (error instanceof HttpErrorResponse) {
            const httpErrorResponse = error as HttpErrorResponse;
            customError = {
                ...httpErrorResponse,
                isServerside: true,
                error: undefined,
                headers: undefined
            };
        } else {
            const clientError = error as Error;
            customError = { isServerside: false, message: clientError.message, name: clientError.name, stack: clientError.stack };
        }

        customError.navigationUrl = window.location.href;

        return customError;
    }
}

## 4. Step: Create Custom Error Type We will create our own error interface to fulfill all requirements to our custom error.

error-handling/custom-error.ts

import { HttpHeaders } from '@angular/common/http';

export interface CustomError {
    name?: string;
    isServerside?: boolean;
    message?: string;
    stack?: string;
    navigationUrl?: string;
    error?: any;
    headers?: HttpHeaders;
    ok?: boolean;
    status?: number;
    statusText?: string;
    requestedUrl?: string | null;

}

Uncomment the import-line to import and use the new custom-error interface within your custom-error-handler.ts

error-handling/custom-error-handler.ts

import { HttpErrorResponse } from '@angular/common/http';
import { ErrorHandler, Injectable, Injector, NgZone } from '@angular/core';
import { Router } from '@angular/router';
import { CustomError } from './custom-error'; // <--- UNCOMMENT THIS LINE!

@Injectable({
    providedIn: 'root'
})
export class CustomErrorHandler implements ErrorHandler {

Replace also the type any within the parseError()-Methode of the custom-error-handler.ts:

   private parseError(error: any): CustomError { // <-- REPLACE ANY WITH CUSTOM-ERROR
        let customError: CustomError; // <-- REPLACE ANY WITH CUSTOM-ERROR

        if (error instanceof HttpErrorResponse) {
            const httpErrorResponse = error as HttpErrorResponse;
            customError = {
                ...httpErrorResponse,
                isServerside: true,
                error: undefined,
                headers: undefined
            };
        } else {
            const clientError = error as Error;
            customError = { isServerside: false, message: clientError.message, name: clientError.name, stack: clientError.stack };
        }

        customError.navigationUrl = window.location.href;

        return customError;
    }

## 5. Step: Create the Testpage We create a Testpage to demonstrate the different errors.

Execute following command to create the testpage-component:

ng g c pages/testPage --skip-tests

Replace the existing html of the test-page.component.ts with following content:

app/pages/test-page/test-page/test-page.component.ts

<div class="title">Testpage</div>

<div class="button-list">
    <button (click)="getUsers()">get users from REST-API</button><br>
    <button (click)="getUsersFailing()">get users from REST-API(fails with 404 status-code)</button><br>
    <button (click)="throwClientError()">throw client error</button>
</div>

<div *ngFor="let user of users">
    {{user.id}}<br>
    {{user.name}}<br>
    {{user.email}}<br>
</div>

Replace the styles of the 'test-page.component.scss' with following:

app/pages/test-page/test-page/test-page.component.scss

.title {
  text-align: center;
  font-size: 2em;
  font-weight: bold;
  margin-bottom: 1em;
}

.button-list {
  width: 50%;
  margin: auto;
  text-align: center;
}

button {
  display: inline-block;
  margin-bottom: 0.5em;
}

Replace the class of the test-page.component.ts with following: app/pages/test-page/test-page/test-page.component.ts

import { Component, ErrorHandler } from '@angular/core';
import { of, map, mergeMap } from 'rxjs';
// import { User } from 'src/app/models/user';
// import { BackendService } from 'src/app/services/backend.service';

@Component({
  selector: 'app-test-page',
  templateUrl: './test-page.component.html',
  styleUrls: ['./test-page.component.scss']
})
export class TestPageComponent {

  users?: any[]; //User[];

  constructor(
    // private backendService: BackendService,
    private errorHandler: ErrorHandler) {

  }

  getUsers(): void {
    /*     this.backendService.getUsers().subscribe({
          next: (users) => this.users = users,
        }); */
  }

  getUsersFailing(): void {
    /*     this.backendService.getUsersFailing().subscribe({
          next: (users) => this.users = users,
        }); */
  }


  throwClientError(): void {
    throw new Error('I\'m a client error!');
  }
}

## 6. Step: Create the BackendService We create a the BackendService to call some data from a public REST-API. With this we can simulate expicitly an backend-error.

But before we create the service we create a specific user-model to bind the data from the REST-API.

Create following file:

app/models/user.ts

export interface User {
    id: number;
    name: string;
    email: string;
    gender: string;
    status: string;
}

Now we can create the BackendService. Execute following command:

ng g s services/backend --skip-tests

Replace the complete code of the service with following:

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { User } from '../models/user';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class BackendService {

  constructor(private httpClient: HttpClient) { }

  public getUsers(): Observable<User[]> {
    return this.httpClient.get<User[]>('https://gorest.co.in/public/v2/users');
  }

  public getUsersFailing(): Observable<User[]> {
    return this.httpClient.get<User[]>('https://gorest.co.in/public/v2/users-let-it-fail');
  }
}

Import the HttpClientModule in the AppModule

  @NgModule({
  declarations: [
    AppComponent,
    TestPageComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    AppRoutingModule,
    ErrorHandlingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Now open the test-page.component.ts again and replace all code with following:

import { Component } from '@angular/core';
import { User } from 'src/app/models/user';
import { BackendService } from 'src/app/services/backend.service';

@Component({
  selector: 'app-test-page',
  templateUrl: './test-page.component.html',
  styleUrls: ['./test-page.component.scss']
})
export class TestPageComponent {

  users?: User[];

  constructor(
    private backendService: BackendService) {

  }

  getUsers(): void {
    this.backendService.getUsers().subscribe({
      next: (users) => this.users = users,
    });
  }

  getUsersFailing(): void {
    this.backendService.getUsersFailing().subscribe({
      next: (users) => this.users = users,
    });
  }


  throwClientError(): void {
    throw new Error('I\'m a client error!');
  }
}

Replace the html of file app.component.html with following:

<router-outlet></router-outlet>

## 7. Step: Create the routing to the TestPage Open the `app-routing-module.ts` and replace everything with:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TestPageComponent } from './pages/test-page/test-page.component';

const routes: Routes = [
  { path: '', redirectTo: '/testpage', pathMatch: 'full' },
  {
    path: 'testpage',
    component: TestPageComponent
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Now you can navigate to http://localhost:<your-port>/testpage and you should see following:

image.png

If you click on the buttons which simulate an error you only see the errors in the console for the moment, but we will change this now.

## 8. Step: Create the routing to the ErrorPage Replace the code of `error-handling-routing.module.ts` with:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ErrorPageComponent } from './pages/error-page/error-page.component';

const routes: Routes = [
  {
    path: 'error',
    component: ErrorPageComponent
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class ErrorHandlingRoutingModule { }

Add the CustomErrorHandler to the Dependency Injector within the ErrorHandlingModule. Replace the whole code with:

error-handling.module.ts

import { ErrorHandler, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { ErrorHandlingRoutingModule } from './error-handling-routing.module';
import { ErrorPageComponent } from './pages/error-page/error-page.component';
import { CustomErrorHandler } from './custom-error-handler';


@NgModule({
  declarations: [
    ErrorPageComponent
  ],
  imports: [
    CommonModule,
    ErrorHandlingRoutingModule
  ],
  providers: [
    { provide: ErrorHandler, useClass: CustomErrorHandler },
  ]
})
export class ErrorHandlingModule { }

## 9. Step: Create the HttpInterceptor to catch the BackendErrors Execute following command to create an HttpInterceptor:

ng g interceptor error-handling/http-interceptors/error --skip-tests

Replace the code in error.interceptor.ts with:

import { ErrorHandler, Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpErrorResponse
} from '@angular/common/http';
import { catchError, EMPTY, Observable } from 'rxjs';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {

  constructor(private errorHandler: ErrorHandler) { }

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    return next.handle(request).pipe(
      catchError((error: HttpErrorResponse) => {
        this.errorHandler.handleError(error);
        return EMPTY;
      })
    );
  }
}

Add the ErrorInterceptor to you ErrorHandlingModule. Replace the ErrorHandlingModule code with following:

import { ErrorHandler, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ErrorPageComponent } from './pages/error-page/error-page.component';
import { ErrorHandlingRoutingModule } from './error-handling-routing.module';
import { CustomErrorHandler } from './custom-error-handler';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { ErrorInterceptor } from './http-interceptors/error.interceptor';



@NgModule({
  declarations: [
    ErrorPageComponent
  ],
  imports: [
    CommonModule,
    ErrorHandlingRoutingModule
  ],
  providers: [
    { provide: ErrorHandler, useClass: CustomErrorHandler },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: ErrorInterceptor,
      multi: true
    }
  ]
})
export class ErrorHandlingModule { }

Replace the complete code of error-page.component.ts with:

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { CustomError } from '../../custom-error';

@Component({
  selector: 'app-error-page',
  templateUrl: './error-page.component.html',
  styleUrls: ['./error-page.component.scss']
})
export class ErrorPageComponent {

  error?: CustomError;

  constructor(private router: Router) {
    // beware to call getCurrentNavigation() within the constructor, everything later
    // is to late, because the navigation step will be executed and the state is lost.
    const state = this.router.getCurrentNavigation()?.extras?.state;
    if (state) {
      this.error = state['lastError'] as CustomError;
    }
  }

  public restartApp(): void {
    window.location.href = '/';
  }

}

WELL DONE!

You will find the complete code of the app on my github-account:
Error-Handling-Demo

I hope this blog was useful for you and I could help you a little bit. All the best for your future journey in your App!