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:
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!