onyks
onyks

Reputation: 11

Angular6. Proper way of error handling in httpClient

I'm wondering about error handling from observable streams. I read article about using rxjs and async pipe in Angular. It was written here we should avoid to use subscribe operator and try to replace it with async pipe. But now when I try to write some post/put request we have to subscribe so i'm little bit confused where i should modify my component state.

submitForm() {
  this.restService.add(this.user.id, this.form.value)
    .pipe(
      tap(() => { this.isAdded = true }),
      catchError((error: HttpErrorResponse) => {
          this.responseMsg = 'Something went wrong'
          return throwError('notFound')
        }
      )
    ).subscribe();
  }

Or should i handle this in subscribe

submitForm() {
      this.restService.add(this.user.id, this.form.value)
        .pipe(
          tap(() => { this.isAdded = true }),
        ).subscribe( 
            () => {}, 
            () => {this.responseMsg = 'Something went wrong'}
       );
 }

Which solution is better?

Upvotes: 1

Views: 14997

Answers (3)

user10271701
user10271701

Reputation:

This is how you do a proper use of async pipe. When you use async pipes you don't have to subscribe because they do the lifcycle of an observable for you. The subscribe and destroy the subscription to avoid memory leaks. If you don't use async pipes you have to do this manually.

SERVICE.TS

import { EquipmentID } from './../../../shared/model/equipmentID.model';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { PickupAvailability } from '../models/pickup-availability.model';
import { catchError } from 'rxjs/operators';
import { ErrorHandlingService } from '../../../shared/services/error-handling.service';
@Injectable()
export class PickupAvaibilityService {
  BASE_URL = '/sxrw-ship/api/v1';
  AMOUNT_DUE_URL = '/shipments/search';

  constructor(
    private http: HttpClient,
    private errorHandler: ErrorHandlingService
  ) {}

  getAmountDue(equipmentID: EquipmentID[]): Observable<PickupAvailability> {
    return this.http
      .post<PickupAvailability>(
        this.BASE_URL + this.AMOUNT_DUE_URL,
        equipmentID
      )
      .pipe(catchError(this.errorHandler.handleError));
  }
}

Then in my custom error handler service you will have something like this:

ERRORHANDLER.SERVICE.TS

import { Injectable } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { _throw } from 'rxjs/observable/throw';
@Injectable()
export class ErrorHandlingService {
  constructor() {}

  handleError(error: HttpErrorResponse) {
    //To know the version of RxJS npm list --depth=0 (I for this example im on version 5.5)
    if (error.error instanceof ErrorEvent) {
      // A client-side or network error occurred. Handle it accordingly.
      console.error('An error occurred: ', error.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 ${error.status}` + ` body was: ${error.message}`
      );
    }
    // return an observable with a user-facing error message
    return _throw('Something bad happened; please try again later.');
  }
}

The way I did my error handler is the way it is done on the Angular's official website https://angular.io/guide/http

Now, in the component that will be using the service that calls us http you declare a variable of type Observable (it's good practice to use the $ sing)

COMPONENT.TS

pickupAvailability$: Observable<PickupAvailability>;

  getPickupDate(pa: PickupAvailability) {
    let id = pa.equipmentInitial + pa.equipmentNumber;
    this.equipmentSummary$ = this.pickupDateService
      .getPickupDate(id)
      .pipe(
        map(
          pd =>
            new EquipmentSummary(
              id,
              pd.pickupDate,
              'TODO',
              pa.totalPremiseCharges
            )
        )
      );
  }

HTML

<div *ngIf="(pickupAvailability$ | async) as pickupAvailability">
  <!-- TODO move to table -->
  <div *ngFor="let pa of pickupAvailability">
    <button pButton type="button" label="Pay ${{ pa.totalPremiseCharges }}" class="x-primary-gray-100" (click)='getPickupDate(pa)'>
    </button>
  </div>
  <div *ngIf="(equipmentSummary$ | async) as es">
    <app-pay-storage [equipmentSummary]="es"></app-pay-storage>
  </div>
</div>

You can test it by doing this:

{{pickupAvailability$ | async | json}}

when you are actually going to use it remove the json pipe

Upvotes: 2

Ayoub k
Ayoub k

Reputation: 8918

If you want to handle just the errors from a backend API (HTTP error) you can just handle them in a subscribe function.

Also you can use a custom error handler to handler all your HTTP errors:

import {ErrorHandler} from "@angular/core";
import {UNAUTHORIZED, BAD_REQUEST, FORBIDDEN} from "http-status-codes";
import {Router} from "@angular/router";
import {ToastsManager, Toast, ToastOptions} from "ng2-toastr";

@Injectable()
export class myAppErrorHandler implements ErrorHandler {

  static readonly REFRESH_PAGE_ON_TOAST_CLICK_MESSAGE: string = "An error occurred: Please click this message to refresh";
  static readonly DEFAULT_ERROR_TITLE: string = "Something went wrong";

  constructor(private router: Router,private toastManager: ToastsManager){};


  public handleError(error: any) {
    console.error(error);
    let httpErrorCode = error.httpErrorCode;
    switch (httpErrorCode) {
      case UNAUTHORIZED:
          this.router.navigateByUrl("/login");
          break;
      case FORBIDDEN:
          this.router.navigateByUrl("/unauthorized");
          break;
      case BAD_REQUEST:
         this.showError(error.message);
          break;
      default:
         this.showError(REFRESH_PAGE_ON_TOAST_CLICK_MESSAGE);
    }
  }

  private showError(message:string){
    this.toastManager.error(message, DEFAULT_ERROR_TITLE, { dismiss: 'controlled'}).then((toast:Toast)=>{
            let currentToastId:number = toast.id;
            this.toastManager.onClickToast().subscribe(clickedToast => {
                if (clickedToast.id === currentToastId) {
                    this.toastManager.dismissToast(toast);
                    window.location.reload();
                }
            });
        });
  }
}

Then tell angular to use it instead of the default error handler, to do that add to your providers section in your app.module.ts the following:

  providers: [
      {provide: ErrorHandler, useClass: myAppErrorHandler }
  ]

Upvotes: 0

Gr&#233;gory Elhaimer
Gr&#233;gory Elhaimer

Reputation: 2801

You can consider using HttpClient interceptor.

@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
  constructor(public auth: AuthService) {}
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

       return next.handle(request)
            .pipe(catchError(err => {
                 // handle your logic there
             }));
   }
}

Doing, all your Http requests will be handled the same way at the same place. You won't need to inject error management services.

More infos there: Angular httpClient interceptor error handling

Upvotes: 1

Related Questions