Ke Vin
Ke Vin

Reputation: 3760

How to catch error response properly in Angular?

i need to catch response body produced by my REST API, which is throwing http error 406 with custom response body, but in my angular, i only can catch the header status which is 'Not Acceptable', i can't get the body, how can i get the body instead of the 'Not Acceptable' string? and i got this error too TypeError: You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable. which is i dunno which part that trigger this error. Here is how i do it :

first i create global service class, which is looked like this :

  constructor(
    private http: HttpClient,
    private alertx: AlertService) { }

    post<T>(url: string, body?: any, header?: HttpHeaders, param?: HttpParams): Observable<HttpResponse<T>>{
      return this.http.post<T>(url, body,
        {headers : header ? header : this.headers,
        observe: 'response',
        responseType: 'json',
        params: param})
        .pipe(catchError(err => {
          const error = err.error?.error_description || err.error?.message || err.statusText;
          console.log(err);
          console.log('here i am');
          return throwError(error);
        }))
        .pipe(timeout(10000), catchError(err => {
          if (err instanceof TimeoutError) {
            this.alertx.error('Timeout Exception');
            return throwError('Timeout Exception');
          }
        }))
        .pipe(shareReplay());
  }

i tried to console.log the error it just appear string 'Not Acceptable', and it continued by console.log('here i am'); it print out properly, and i can't find where the cause of TypeError

here is how i use function above :

  requestCancel(data: SelectItem, reasonCancel: string): Observable<any> {
      const request: RequestResponse<SelectItem> = new RequestResponse<SelectItem>(data);
      request.message = reasonCancel;
      return this.rest.post<any>(`${environment.apiUrl}${endpoint.sales_order.request_cancel}`, request).pipe(map(
          response => {
              return response.body.data;
          }));
  }

when i trying to console.log the response, it didn't appear. I think my function stop working in my post function at my global service class, what did i do wrong?

------------------ UPDATE OF MY CODE --------------------

i create HttpRequestInterceptor class to handle the error, here is my class :

@Injectable()
export class HttpRequestInterceptor implements HttpInterceptor {

  user: UserModel;
  prevUrl: string = '';
  constructor(
    private loginSvc: LoginService, 
    private alertx: AlertService,
    private sharedService: SharedService) {
    this.sharedService.currentUserSubject.subscribe(user => this.user = user);
  }

  intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    this.sharedService.setLoading(true, httpRequest.url);
    return next.handle(httpRequest).pipe(catchError(err => {
      if ([401, 403].includes(err.status) /* && this.user */) {
        // auto logout if 401 or 403 response returned from api
        this.sharedService.setLoading(false, httpRequest.url);
        this.loginSvc.removeLocal();
      }
      this.sharedService.setLoading(false, httpRequest.url);
      const error = err.statusText || err.statusText + ' ' + err.error?.message || err.statusText + ' ' + err.error?.error_description;
      this.alertx.error(error, {autoClose: true});
      return throwError(err.error?.message);
    }))
      .pipe(map<HttpEvent<any>, any>((evt: HttpEvent<any>) => {
        if (evt instanceof HttpResponse) {
          this.sharedService.setLoading(false, httpRequest.url);
        }
        return evt;
      }));
  }
}

when i console.log my err.error?.message i can catch the error properly, then i the value passed to here :

    post<T>(url: string, body?: any, header?: HttpHeaders, param?: HttpParams): Observable<HttpResponse<T>>{
      return this.http.post<T>(url, body,
        {headers : header ? header : this.headers,
        observe: 'response',
        responseType: 'json',
        params: param})
        .pipe(catchError(err => {
          console.log(err);
          return throwError(err);
        }))
        .pipe(timeout(10000), catchError(err => {
          if (err instanceof TimeoutError) {
            this.alertx.error('Timeout Exception');
            return throwError('Timeout Exception');
          }
        }))
        .pipe(shareReplay());
  }

until there i still can catch the error properly when i console.log(err) but when i consume the post function, i can't catch the error, and i got error in my browser console. Here is my function :

  requestCancel(data: SelectItem, reasonCancel: string): Observable<any> {
      const request: RequestResponse<SelectItem> = new RequestResponse<SelectItem>(data);
      request.message = reasonCancel;
      return this.rest.post<any>(`${environment.apiUrl}${endpoint.sales_order.request_cancel}`, request).pipe(map(
          response => {
              return response.body.data;
          }))
          .pipe(catchError(err => {
              // i cannot get anything here
              console.log(err);
              return err;
      }));
  }

i can't log my error, it doesn't event print anything, i think my function crashed and throw this error instead :

TypeError: You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.

what did i still missed here?

Upvotes: 1

Views: 16133

Answers (2)

Timothy
Timothy

Reputation: 3593

You can write an interceptor to handle errors in 1 place. It would be more convenient, because you will not have to handle simular scenarios in different services

@Injectable({
  providedIn: 'root',
})
export class RequestInterceptor implements HttpInterceptor {
  public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      tap( // or catchError operator
        (event: HttpEvent<any>) => event,
        (error: any) => {
          if (error instanceof HttpErrorResponse) {
            if (error.status === 406) {
              // do something here
            }
          }
        }
      )
    );
  }
}

And add RequestInterceptor into array of providers of AppModule

@NgModule({
    imports: [
        ...
    ],
    declarations: [
        ...
    ],
    providers: [
        { provide: HTTP_INTERCEPTORS, useClass: RequestInterceptor, multi: true }
    ],

    bootstrap: [AppComponent]
})
export class AppModule {}

Upvotes: 2

Salahuddin Ahmed
Salahuddin Ahmed

Reputation: 5658

As agreed with Timothy's answer, HttpInterceptor is convenient to handle error from a single place which provides a way to intercept HTTP requests and responses to transform or handle them before passing them along.

There are two use cases that can be implemented in the interceptor.

First, retry the HTTP call once or multiple times before throwing the error. In some cases, for example, if there is a timeout, better to continue without throwing the exception.

For this, use the retry operator from RxJS to resubscribe to the observable.

Then check the status of the exception and see if it is a 401 unauthorized error. Then check the status of the exception and see if it is a 406 not acceptable error. With token-based security, try to refresh the token, if required. If this does not work, redirect the user to the login page or else.

import { Injectable } from '@angular/core';
import { 
  HttpEvent, HttpRequest, HttpHandler, 
  HttpInterceptor, HttpErrorResponse 
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { retry, catchError } from 'rxjs/operators';

@Injectable()
export class ServerErrorInterceptor implements HttpInterceptor {

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

    return next.handle(request).pipe(
      retry(1),
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401) {
          // refresh token
        } 
        else if (error.status === 406) {
          // do something here
        } 
        else {
          return throwError(error);
        }
      })
    );    
  }
}

Here you may retry once before you check the error status and rethrow the error. Refreshing security tokens is up to you, if required.

Upvotes: 1

Related Questions