In the tutorial, we show how to handle error from Angular HttpClient with catchError
& retry
when request fails on the SpringBoot server, or in case of a poor network connection.
Related posts:
– Angular 6 HttpClient – Get/Post/Put/Delete requests + SpringBoot RestAPIs + Bootstrap 4
Contents
Technologies
- Angular 6
- RxJS 6
- Bootstrap 4
- Visual Studio Code – version 1.24.0
- SpringBoot
Error Handling
Error Object
Simple Angular HttpClient to request as below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Injectable({ providedIn: 'root' }) export class CustomerService { private customersUrl = 'http://localhost:8080/api/customers'; // URL to web api constructor( private http: HttpClient ) { } getCustomers (): Observable<Customer[]> { return this.http.get<Customer[]>(this.customersUrl); } } |
What happens if the request fails on the server, or if a poor network connection?
– Server is die -> console’s logs:
– 404 error -> console’s logs:
– 500 error -> console’s logs:
-> HttpClient
will return an error
object.
We can handle it from Component
code with .subcribe
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
export class CustomerComponent implements OnInit { ... getCustomers() { return this.customerService.getCustomers() .subscribe( customers => { // success path // to do console.log(customers); }, error => { // error path this.error = error } ); } } |
Error Details
Angular HttpClient provides HttpErrorResponse
to capture error responses. We can use HttpErrorResponse
to build detail user-friendly responses:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import { Injectable } from '@angular/core'; import { HttpErrorResponse } from '@angular/common/http'; export type HandleError = <T> (operation?: string, result?: T) => (error: HttpErrorResponse) => Observable<T>; /** Handles HttpClient errors */ @Injectable() export class HttpErrorHandler { constructor(private errorService: ErrorService) { } /** Create handleError function that already knows the service name */ createHandleError = (serviceName = '') => <T> (operation = 'operation', result = {} as T) => this.handleError(serviceName, operation, result); /** * @param serviceName: name of the data service * @param operation: name of the failed operation * @param result: optional value to return as the observable result */ handleError<T> (serviceName = '', operation = 'operation', result = {} as T) { return (error: HttpErrorResponse): Observable<T> => { // Todo -> Send the error to remote logging infrastructure console.error(error); // log to console instead const message = (error.error instanceof ErrorEvent) ? error.error.message : `{error code: ${error.status}, body: "${error.message}"}`; // -> Return a safe result. return of( result ); }; } } |
-> Now we pipe returned Observables
with the error handler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
export class CustomerComponent implements OnInit { private handleError: HandleError; constructor( private http: HttpClient, httpErrorHandler: HttpErrorHandler ) { this.handleError = httpErrorHandler.createHandleError('CustomerService'); } ... getCustomers (): Observable<Customer[]> { return this.http.get<Customer[]>(this.customersUrl) .pipe( catchError(this.handleError('getCustomers', [])) ); } |
Retry
The RxJS library offers retry
operator to automatically re-subscribes to a failed Observable
with a specified number of times.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
export class CustomerComponent implements OnInit { private handleError: HandleError; constructor( private http: HttpClient, httpErrorHandler: HttpErrorHandler ) { this.handleError = httpErrorHandler.createHandleError('CustomerService'); } ... getCustomers (): Observable<Customer[]> { return this.http.get<Customer[]>(this.customersUrl) .pipe( retry(3), // retry a failed request up to 3 times catchError(this.handleError('getCustomers', [])) // then handle the error ); } |
Practice
We re-use the source-codes of Angular 6 HttpClient – Get/Post/Put/Delete requests + SpringBoot RestAPIs + Bootstrap 4 tutorial.
How to handle error?
-> In Angular project, we need build a HttpErrorHandler
service and ErrorComponent
to notify on UI.
– Angular Project as below:
– SpringBoot project:
Implement Now ->
Generate HttpErrorHandler
, Error
service, Error
component by cmd:
1 2 3 |
ng generate service HttpErrorHandler ng generate service Error ng generate component Error |
HttpErrorHandler Service
Implement the service ./src/app/http-error-handler.service.ts
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import { Injectable } from '@angular/core'; import { HttpErrorResponse } from '@angular/common/http'; import { Observable, of } from 'rxjs'; import { ErrorService } from './error.service'; export type HandleError = <T> (operation?: string, result?: T) => (error: HttpErrorResponse) => Observable<T>; /** Handles HttpClient errors */ @Injectable() export class HttpErrorHandler { constructor(private errorService: ErrorService) { } /** Create handleError function that already knows the service name */ createHandleError = (serviceName = '') => <T> (operation = 'operation', result = {} as T) => this.handleError(serviceName, operation, result); /** * @param serviceName: name of the data service * @param operation: name of the failed operation * @param result: optional value to return as the observable result */ handleError<T> (serviceName = '', operation = 'operation', result = {} as T) { return (error: HttpErrorResponse): Observable<T> => { // Todo -> Send the error to remote logging infrastructure console.error(error); // log to console instead const message = (error.error instanceof ErrorEvent) ? error.error.message : `{error code: ${error.status}, body: "${error.message}"}`; // Todo -> Transforming error for user consumption this.errorService.errorMessage = `${serviceName} -> ${operation} failed.\n Message: ${message}`; // -> Return a safe result. return of( result ); }; } } |
Add HttpErrorHandler
service to providers of AppModule
module:
1 2 3 4 5 6 7 8 9 10 11 12 |
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpErrorHandler } from'./http-error-handler.service'; @NgModule({ ... providers: [HttpErrorHandler], ... }) export class AppModule { } |
Update Customer Service
./app/customer.service.ts
->
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs'; import { catchError, retry } from 'rxjs/operators'; import { Customer } from './customer'; import { HttpErrorHandler, HandleError } from './http-error-handler.service'; const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) }; @Injectable({ providedIn: 'root' }) export class CustomerService { private customersUrl = 'http://localhost:8080/api/customers'; // URL to web api private handleError: HandleError; constructor( private http: HttpClient, httpErrorHandler: HttpErrorHandler ) { this.handleError = httpErrorHandler.createHandleError('CustomerService'); } getCustomers (): Observable<Customer[]> { return this.http.get<Customer[]>(this.customersUrl) .pipe( retry(3), catchError(this.handleError('getCustomers', [])) ); } getCustomer(id: number): Observable<Customer> { const url = `${this.customersUrl}/${id}`; return this.http.get<Customer>(url) .pipe( retry(3), catchError(this.handleError('addCustomer', null)) ); } addCustomer (customer: Customer): Observable<Customer> { return this.http.post<Customer>(this.customersUrl, customer, httpOptions) .pipe( retry(3), catchError(this.handleError('addCustomer', customer)) ); } deleteCustomer (customer: Customer | number): Observable<{}> { const id = typeof customer === 'number' ? customer : customer.id; const url = `${this.customersUrl}/${id}`; return this.http.delete(url, httpOptions) .pipe( retry(3), catchError(this.handleError('deleteCustomer', null)) ); } updateCustomer (customer: Customer): Observable<any> { return this.http.put(this.customersUrl, customer, httpOptions) .pipe( catchError(this.handleError('updateCustomer', null)) ); } } |
Error Service
./src/app/error.service.ts
->
1 2 3 4 5 6 7 8 9 10 11 |
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class ErrorService { errorMessage: string = ""; constructor() { } } |
Error Handler Component
./app/error/error.component.ts
->
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import { Component, OnInit } from '@angular/core'; import { ErrorService } from '../error.service' @Component({ selector: 'app-error', templateUrl: './error.component.html', }) export class ErrorComponent implements OnInit { constructor(public errorService: ErrorService) { } ngOnInit() { } } |
./app/error/error.component.html
->
1 2 3 |
<div> <h3 style="color:red">{{errorService.errorMessage}}</h3> </div> |
Run & Check Results
Server Die
Start SpringBoot server.
Run Angular Client with cmd -> ng serve
-> results:
Stop SpringBoot server. Then make the request again ->
404 Error
In SpringBoot project, modify method getAll
in file RestAPIs.java
:
1 2 3 4 5 6 7 8 9 10 |
@GetMapping(value="/api/customers") public ResponseEntity<?> getAll(HttpServletResponse res){ /*List<Customer> results = customers.entrySet().stream() .map(entry ->entry.getValue()) .collect(Collectors.toList()); return results;*/ res.setStatus(HttpServletResponse.SC_NOT_FOUND); return null; } |
Restart SpringBoot server -> 404
error:
500 Error
In SpringBoot project, modify method getAll
in file RestAPIs.java
:
1 2 3 4 5 6 7 8 9 10 |
@GetMapping(value="/api/customers") public ResponseEntity<?> getAll(HttpServletResponse res){ /*List<Customer> results = customers.entrySet().stream() .map(entry ->entry.getValue()) .collect(Collectors.toList()); return results;*/ res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return null; } |
Restart SpringBoot server -> 500
error: