Error Handling with Angular 6 - Tips and Best Practices

Publikováno: 20.11.2018

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 d...

Celý článek

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, which is covered in this article.

An application that does not handle errors gracefully leaves its users confused and frustrated when the app suddenly breaks without explanation. Handling these errors across an application greatly improves user experience. Data can also be collected from within the error handlers to inform the development team about important issues that slipped past testing.

In this article, we will compare several solutions for error handling in Angular apps. First we will describe traditional approaches using ErrorHandler and HttpClient, and then we will show you a better solution using HttpInterceptor. We'll also show how you can use this interceptor to track errors centrally in [Rollbar](https://rollbar.com/error-tracking/angular/).

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. When those clients experience errors, you'll have no visibility to them unless you record them in a centralized location. 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 runtime errors as well. In order to catch uncaught errors, you need to implement an error handler, which we will describe next.

The shortcomings of 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 earlier tutorial on how to use [ErrorHandler in Angular 2+](https://rollbar.com/blog/client-side-angular-error-handling/).

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

Handling errors with HttpClient

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 checking 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 something needs to change with the way we handle errors, 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);
 }
}

Full Angular Example: [https://stackblitz.com/edit/error-handling-httpclient](https://stackblitz.com/edit/error-handling-httpclient)

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 a lot more maintainable - as we create new services, they will automatically handle errors. We are even able to add the `retry(1)` function to our interceptor, so all http requests will be retried once before failing. Now that we have this in one 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, 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)
 }
}

**Full Angular Example: [https://stackblitz.com/edit/error-handling-httpinterceptor ](https://stackblitz.com/edit/error-handling-httpinterceptor)**

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](https://rollbar.com/error-tracking/angular/). It supports source maps so you can see the exact line in your source code creating the error. It also integrates with GitHub to show you the surrounding code. [Telemetry](https://docs.rollbar.com/docs/rollbarjs-telemetry) is an especially useful feature because it shows you what the user was doing before the error occurred. This helps you 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](https://rollbar.com/signup/). Then, after naming your project, select the "Angular 2+" SDK.

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

Your app should be set up approximately like 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 * as Rollbar from 'rollbar';

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

const rollbarConfig = {
 accessToken: 'YOUR_ACCESS_TOKEN_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 { 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({ providedIn: 'root' })
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(error)
         return throwError(errorMessage);
       })
     )
 }
}

Full Angular Example: [https://stackblitz.com/edit/error-handling-httpinterceptor-rollbar\]\(https://stackblitz.com/edit/error-handling-httpinterceptor-rollbar)

Be sure to replace `YOUR_ACCESS_TOKEN_HERE` with the access token provided in your Rollbar config.

Viewing Tracked Errors on Rollbar

Now that Rollbar is set up, you should start to see errors show up in your dashboard:

From the dashboard, you can quickly see the most critical errors and how often they occur. By clicking an individual item, you can bring up deep details explaining the full error message, which browsers and systems are throwing it, and much more. From this page you can also assign the error to a developer, turn on activity notifications, and mark the error as resolved when its fixed.

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 occured or at least have an error code to give to your support team, resulting in much faster issue resolution. 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 of these errors across our app with advanced features for monitoring, analysing, and triaging. [Rollbar's SDK for Angular](https://rollbar.com/error-tracking/angular/) makes it easy to integrate these features into the global HttpInterceptor to quickly improve the development and user experience of our application.

Nahoru
Tento web používá k poskytování služeb a analýze návštěvnosti soubory cookie. Používáním tohoto webu s tímto souhlasíte. Další informace