Programming
Catching errors in Angular HttpClient
Building robust web applications with Angular often hinges on how effectively they communicate with backend services. While the HttpClient module in Angular simplifies making HTTP requests, the real challenge and mark of a mature application lies in adeptly catching errors in Angular HttpClient operations. Unhandled errors can lead to a poor user experience, broken functionality, and a frustrating debugging process. This guide delves into the essential techniques and best practices for managing HTTP errors, ensuring your Angular applications remain resilient, user-friendly, and maintainable even when external APIs or network conditions falter.
Why Robust Error Handling is Crucial in Angular Apps
In the dynamic world of web development, network requests are inherently susceptible to failure. Whether it’s a server-side issue, an invalid API endpoint, or simply a user’s unstable internet connection, your application must be prepared to gracefully handle these scenarios. Effective API error management is not merely a technical detail; it directly impacts the perceived quality and reliability of your software. A well-implemented error handling strategy prevents application crashes, provides meaningful feedback to users, and significantly reduces the time developers spend on troubleshooting.
Consider the alternative: an application that silently fails or displays cryptic error messages. This can lead to user frustration, abandonment, and a loss of trust. By proactively anticipating and managing potential pitfalls, developers can ensure a smoother user experience, even when things go wrong under the hood. Beyond user satisfaction, robust error handling aids in debugging and maintenance, allowing for clearer insights into what went wrong and where, streamlining the entire development lifecycle.
According to a study by Nielsen Norman Group, well-designed error messages can significantly improve user satisfaction and task completion rates. This highlights that technical solutions must be paired with user-centric messaging to truly achieve robust error handling. Investing time in comprehensive error management is an investment in your application’s stability and your users’ confidence.
Basic Error Handling with catchError and throwError
The cornerstone of catching errors in Angular HttpClient lies within the powerful RxJS library, which Angular leverages extensively for asynchronous operations. When an HTTP request fails, the HttpClient returns an Observable that emits an error notification rather than a successful response. To intercept and manage this error, we typically use the catchError operator from RxJS, often in conjunction with throwError.
The catchError operator allows you to intercept an error notification and return a new observable, effectively “catching” the error and preventing it from propagating further down the observable chain. This is crucial because an unhandled error will cause the subscriber to unsubscribe, potentially leaving parts of your application in an inconsistent state. Within the catchError block, you receive an HttpErrorResponse object, which provides detailed information about the error, such as the status code, error message, and the original request.
To effectively handle an error originating from an Angular HttpClient request, you typically utilize the catchError operator from RxJS within your service. This operator intercepts the error emitted by the Observable, allowing you to log it, transform it, or display a user-friendly message, before re-throwing a new error Observable using throwError to ensure subscribers are aware of the failure and can react accordingly. For more in-depth knowledge on RxJS error handling, refer to the official RxJS documentation for catchError.
Here’s a conceptual example of how you might implement basic error handling within an Angular service:
import { Injectable } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class DataService { private apiUrl = '/api/data'; constructor(private http: HttpClient) { } getData(): Observable<any> { return this.http.get<any>(this.apiUrl).pipe( catchError(this.handleError) ); } private handleError(error: HttpErrorResponse) { if (error.status === 0) { // A client-side or network error occurred. Handle it accordingly. console.error('An error occurred:', error.error); } else { // The backend returned an unsuccessful response code. // The response body may contain clues as to what went wrong. console.error( Backend returned code ${error.status}, body was: , error.error); } // Return an observable with a user-facing error message. return throwError(() => new Error('Something bad happened; please try again later.')); } }
Advanced Strategies: Interceptors Question & Answer :
I have a data service that looks like this:
@Injectable() export class DataService { baseUrl = 'http://localhost' constructor( private httpClient: HttpClient) { } get(url, params): Promise<Object> { return this.sendRequest(this.baseUrl + url, 'get', null, params) .map((res) => { return res as Object }) .toPromise(); } post(url, body): Promise<Object> { return this.sendRequest(this.baseUrl + url, 'post', body) .map((res) => { return res as Object }) .toPromise(); } patch(url, body): Promise<Object> { return this.sendRequest(this.baseUrl + url, 'patch', body) .map((res) => { return res as Object }) .toPromise(); } sendRequest(url, type, body, params = null): Observable<any> { return this.httpClient[type](url, { params: params }, body) } }
If I get an HTTP error (i.e. 404), I get a nasty console message: ERROR Error: Uncaught (in promise): [object Object] from core.es5.js How do I handle it in my case?
You have some options, depending on your needs. If you want to handle errors on a per-request basis, add a catch to your request. If you want to add a global solution, use HttpInterceptor.
Open here the working demo plunker for the solutions below.
tl;dr
In the simplest case, you’ll just need to add a .catch() or a .subscribe(), like:
import 'rxjs/add/operator/catch'; // don't forget this, or you'll get a runtime error this.httpClient .get("data-url") .catch((err: HttpErrorResponse) => { // simple logging, but you can do a lot more, see below console.error('An error occurred:', err.error); }); // or this.httpClient .get("data-url") .subscribe( data => console.log('success', data), error => console.log('oops', error) );
But there are more details to this, see below.
Method (local) solution: log error and return fallback response
If you need to handle errors in only one place, you can use catch and return a default value (or empty response) instead of failing completely. You also don’t need the .map just to cast, you can use a generic function. Source: Angular.io - Getting Error Details.
So, a generic .get() method, would be like:
import { Injectable } from '@angular/core'; import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/catch'; import 'rxjs/add/observable/of'; import 'rxjs/add/observable/empty'; import 'rxjs/add/operator/retry'; // don't forget the imports @Injectable() export class DataService { baseUrl = 'http://localhost'; constructor(private httpClient: HttpClient) { } // notice the <T>, making the method generic get<T>(url, params): Observable<T> { return this.httpClient .get<T>(this.baseUrl + url, {params}) .retry(3) // optionally add the retry .catch((err: HttpErrorResponse) => { if (err.error instanceof Error) { // A client-side or network error occurred. Handle it accordingly. console.error('An error occurred:', err.error.message); } else { // The backend returned an unsuccessful response code. // The response body may contain clues as to what went wrong, console.error(`Backend returned code ${err.status}, body was: ${err.error}`); } // ...optionally return a default fallback value so app can continue (pick one) // which could be a default value // return Observable.of<any>({my: "default value..."}); // or simply an empty observable return Observable.empty<T>(); }); } }
Handling the error will allow you app to continue even when the service at the URL is in bad condition.
This per-request solution is good mostly when you want to return a specific default response to each method. But if you only care about error displaying (or have a global default response), the better solution is to use an interceptor, as described below.
Run the working demo plunker here.
Advanced usage: Intercepting all requests or responses
Once again, Angular.io guide shows:
A major feature of
@angular/common/httpis interception, the ability to declare interceptors which sit in between your application and the backend. When your application makes a request, interceptors transform it before sending it to the server, and the interceptors can transform the response on its way back before your application sees it. This is useful for everything from authentication to logging.
Which, of course, can be used to handle errors in a very simple way (demo plunker here):
import { Injectable } from '@angular/core'; import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse, HttpErrorResponse } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/catch'; import 'rxjs/add/observable/of'; import 'rxjs/add/observable/empty'; import 'rxjs/add/operator/retry'; // don't forget the imports @Injectable() export class HttpErrorInterceptor implements HttpInterceptor { intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(request) .catch((err: HttpErrorResponse) => { if (err.error instanceof Error) { // A client-side or network error occurred. Handle it accordingly. console.error('An error occurred:', err.error.message); } else { // The backend returned an unsuccessful response code. // The response body may contain clues as to what went wrong, console.error(`Backend returned code ${err.status}, body was: ${err.error}`); } // ...optionally return a default fallback value so app can continue (pick one) // which could be a default value (which has to be a HttpResponse here) // return Observable.of(new HttpResponse({body: [{name: "Default value..."}]})); // or simply an empty observable return Observable.empty<HttpEvent<any>>(); }); } }
Providing your interceptor: Simply declaring the HttpErrorInterceptor above doesn’t cause your app to use it. You need to wire it up in your app module by providing it as an interceptor, as follows:
import { NgModule } from '@angular/core'; import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { HttpErrorInterceptor } from './path/http-error.interceptor'; @NgModule({ ... providers: [{ provide: HTTP_INTERCEPTORS, useClass: HttpErrorInterceptor, multi: true, }], ... }) export class AppModule {}
Note: If you have both an error interceptor and some local error handling, naturally, it is likely that no local error handling will ever be triggered, since the error will always be handled by the interceptor before it reaches the local error handling.
Run the working demo plunker here.