Error Handling with Angular 8 - Tips and Best Practices

Error Handling with Angular 8 - Tips and Best Practices

December 16th, 2019 • By Nicholas Vincent-Hill

Handling errors properly is essential in building a robust application in Angular. Error handlers provide an opportunity to present friendly information to the user and collect important data for development. In today's age of advanced front-end websites, it's more important than ever to have an effective client-side solution for error handling.

An application that does not handle errors gracefully leaves its users confused and frustrated when the app suddenly breaks without explanation. Handling these errors correctly across an application greatly improves user experience. Collected data from the error handling can inform the development team about important issues that slipped past testing. This is why monitoring tools like Rollbar are so important.

In this article, we will compare several solutions for error handling in Angular apps. First, we will describe the traditional approaches using ErrorHandler and HttpClient. Then, we will show you a better solution using HttpInterceptor. We'll also show you how to use this interceptor to monitor and track errors centrally in Rollbar.

The Shortcomings of console.log()

Beginners in JavaScript programming often start out using the console log because that is the default output in most development environments. Once you deploy your application to a production environment, you no longer have access to the console log. That's because the code is now running on the client browser. Unless you record the errors that clients experience in a centralized location, you won't have any visibility into them. In order to understand the user experience and how errors can affect it, you need to track errors centrally. That means not only tracking caught errors, but uncaught or run-time errors as well. In order to catch uncaught errors, you need to implement an error handler, which we will describe next.

The Shortcomings of Angular's ErrorHandler

One traditional way of handling errors in Angular is to provide an ErrorHandler class. This class can be extended to create your own global error handler. This is also a useful way to handle all errors that occur, but is mostly useful for tracking error logs. For reference, you can check our tutorial on how to use ErrorHandler in Angular 2+.

By implementing error handling in HttpClient or HttpInterceptor, you can work directly with all HTTP requests in your application, providing you with the ability to transform the request, retry it, and more. ErrorHandler is useful for more generic error handling, however, HttpInterceptor provides a much more robust way to handle errors related to the server and network.

Handling Errors with HttpClient

By using Angular's HttpClient along with catchError from RxJS, we can easily write a function to handle errors within each service. HttpClient will also conveniently parse JSON responses and return a JavaScript object in the observable. There are two categories of errors which need to be handled differently:

Client-side: Network problems and front-end code errors. With HttpClient, these errors return ErrorEvent instances. Server-side: AJAX errors, user errors, back-end code errors, database errors, file system errors. With HttpClient, these errors return HTTP Error Responses.

By verifying if an error is an instance of ErrorEvent, we can figure out which type of error we have and handle it accordingly.

This is a good solution for just one service, but a real app contains numerous services which can all potentially throw errors. Unfortunately, this solution requires copying the handleError function across all services, which is a very serious anti-pattern in Angular development. If the way we handle errors needs to change, then we have to update every single handleError function across every service. This is counter-productive and can easily lead to more bugs. We need an efficient way to handle errors globally across the entire application. Fortunately, Angular supports this using HttpInterceptor.

Here is an example service which uses this basic form of error handling:

import { Injectable } from '@angular/core';

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

import { Observable, throwError } from 'rxjs';

import { retry, catchError } from 'rxjs/operators';

import { User } from './user.model';



@Injectable({

 providedIn: 'root'

})

export class UserService {

 private apiUrl = 'https://localhost:8080/api/users';



 constructor(private http: HttpClient) {}



 getUsers(): Observable<User[]> {

   return this.http.get<User[]>(this.apiUrl).pipe(

     retry(1),

     catchError(this.handleError)

   );

 }



 handleError(error) {

   let errorMessage = '';

   if (error.error instanceof ErrorEvent) {

     // client-side error

     errorMessage = `Error: ${error.error.message}`;

   } else {

     // server-side error

     errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;

   }

   window.alert(errorMessage);

   return throwError(errorMessage);

 }

}

Basic error handling source code - https://stackblitz.com/edit/error-handling-in-angular-8

A Better Solution with HttpInterceptor

HttpInterceptor was introduced with Angular 4.3.1. It provides a way to intercept HTTP requests and responses to transform or handle them before passing them along. This is a very useful tool in general; we can modify headers, add authentication tokens, modify data format, and more. By registering these interceptors in our root module, we can handle everything in the application, even with lazy loading implemented.

As you will see below, this makes our service much cleaner. Our application is now easier to maintain. As we create new services, they will automatically handle errors. We are even able to add the retry(1) function to our interceptor to retry all requests once before failing. Now that we have this in place, if we want to modify our error handling, we can simply go to this one file and update it globally across our application. This is definitely the "Angular way" to handle this problem.

In the following example, we implement an interceptor to handle errors across our application:

http-error.interceptor.ts

import {

 HttpEvent,

 HttpInterceptor,

 HttpHandler,

 HttpRequest,

 HttpResponse,

 HttpErrorResponse

} from '@angular/common/http';

import { Observable, throwError } from 'rxjs';

import { retry, catchError } from 'rxjs/operators';



export class HttpErrorInterceptor implements HttpInterceptor {

 intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

   return next.handle(request)

     .pipe(

       retry(1),

       catchError((error: HttpErrorResponse) => {

         let errorMessage = '';

         if (error.error instanceof ErrorEvent) {

           // client-side error

           errorMessage = `Error: ${error.error.message}`;

         } else {

           // server-side error

           errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;

         }

         window.alert(errorMessage);

         return throwError(errorMessage);

       })

     )

 }

}

app.module.ts

import { NgModule } from '@angular/core';

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

import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';



import { AppComponent } from './app.component';

import { HttpErrorInterceptor } from './http-error.interceptor';



@NgModule({

 imports:      [ BrowserModule, HttpClientModule ],

 declarations: [ AppComponent ],

 bootstrap:    [ AppComponent ],

 providers: [

   {

     provide: HTTP_INTERCEPTORS,

     useClass: HttpErrorInterceptor,

     multi: true

   }

 ]

})

export class AppModule { }

user.service.ts

import { Injectable } from '@angular/core';

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

import { Observable } from 'rxjs';

import { User } from './user.model';



@Injectable({

 providedIn: 'root'

})

export class UserService {

 private apiUrl = 'https://localhost:8080/api/users';



 constructor(private http: HttpClient) { }



 getUsers(): Observable<User[]> {

   return this.http.get<User[]>(this.apiUrl)

 }

}

HttpInterceptor source code - https://stackblitz.com/edit/error-handling-httpinterceptor-angular-8

Tracking Angular Errors with Rollbar

Now that we're properly handling errors globally and reporting them to the user, we should take advantage of the opportunity to track these errors and use them to improve development. Within the HttpInterceptor, we can send error logs to a server to keep track of them. However, rather than creating our own server to track logs, we can use Rollbar, an existing service built for this exact use case.

Rollbar provides real-time exception tracking for Angular. It supports source maps so you can see where the error occurs in the exact line in your source code. It also integrates with GitHub to show you the surrounding code. The Telemetry feature is especially useful because it shows you what the user was doing before the error occurred. This helps you to identify the cause of problems faster. It has an excellent SDK that integrates right into our Angular application, which we can use inside the HttpInterceptor to track all of our errors.

The first step is to sign up for a Rollbar account. Then, after naming your project, select the "Angular 2+" SDK.

rollbar-project-onboarding

Once you hit continue, Rollbar will provide configuration information to add into your app.

Your app should be set up similar to this:

app.module.ts

import { NgModule, InjectionToken } from '@angular/core';

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

import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';

import Rollbar from 'rollbar';



import { AppComponent } from './app.component';

import { HttpErrorInterceptor } from './http-error.interceptor';



const rollbarConfig = {

 accessToken: 'YOURTOKEN_HERE',

 captureUncaught: true,

 captureUnhandledRejections: true,

};



export function rollbarFactory() {

 return new Rollbar(rollbarConfig)

}



export const RollbarService = new InjectionToken<Rollbar>('rollbar');



@NgModule({

 imports:      [ BrowserModule, HttpClientModule ],

 declarations: [ AppComponent ],

 bootstrap:    [ AppComponent ],

 providers: [

   {

     provide: RollbarService,

     useFactory: rollbarFactory

   },

   {

     provide: HTTP_INTERCEPTORS,

     useClass: HttpErrorInterceptor,

     multi: true

   }

 ]

})

export class AppModule { }

http-error.interceptor.ts

import * as Rollbar from 'rollbar';

import { Injectable, Injector } from '@angular/core';

import {

 HttpEvent,

 HttpInterceptor,

 HttpHandler,

 HttpRequest,

 HttpResponse,

 HttpErrorResponse

} from '@angular/common/http';

import { RollbarService } from './app.module';

import { Observable, throwError } from 'rxjs';

import { retry, catchError } from 'rxjs/operators';



@Injectable()

export class HttpErrorInterceptor implements HttpInterceptor {

 constructor(private injector: Injector) { }



 intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

   return next.handle(request)

     .pipe(

       retry(1),

       catchError((error: HttpErrorResponse) => {

         const rollbar = this.injector.get(RollbarService);

         let errorMessage = '';

         if (error.error instanceof ErrorEvent) {

           // client-side error

           errorMessage = `Error: ${error.error.message}`;

         } else {

           // server-side error

           errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;

         }

         window.alert(errorMessage);

         rollbar.error(new Error(error.message).stack)

         return throwError(errorMessage);

       })

     )

 }

}

*Full example with Rollbar - https://stackblitz.com/edit/error-handling-httpinterceptor-rollbar-angular-8

Be sure to replace YOURACCESSTOKEN_HERE with the access token provided in your Rollbar config.*

Viewing Tracked Errors on Rollbar

Now that Rollbar is set up, we should start to see errors show up in the dashboard. Here we can quickly see the most critical errors and how often they occur.

rollbar-items-overview

By clicking the individual item for "Error: Http failure response", we can bring up granular details explaining the full error message, which browsers and systems are throwing it, and much more. From this page we can also assign the error to a developer, turn on activity notifications, and mark the error as resolved when it's fixed.

rollbar-error-with-stack-trace

Rollbar also offers telemetry showing you what happened before and after this error, which gives you more context to understand the cause and effect. In this case, we can see that clicking on a particular button triggered an XHR request that resulted in the error message. It offers more detail than what you'd get in a regular log message.

rollbar-error-telemetry

Conclusion

Handling errors properly is essential in building a high-quality user experience. By providing readable messages to users, they can either understand why the error occurred or at least have an error code to give to your support team, which can help resolve issues that much faster. While ErrorHandler is a useful way to handle errors across an app, HttpInterceptor provides a much more robust solution for handling server and connection-related errors giving the ability to retry or return a richer error response to the client.

By setting up our application with Rollbar, we get real-time tracking on all errors that occur across our app as well as advanced features for monitoring, analyzing, and diagnosing. Rollbar's SDK for Angular makes it easy to integrate these features into the global HttpInterceptor to quickly improve the development and user experience of our application.

Get the latest updates delivered to your inbox.