angularerror-handlingrxjsobservablesingle-page-application

Rethrowing Errors When Using forkJoin


I'm working on an Angular (v18) SPA app. I have an API service with a get call:

  public get(route: string, params?: HttpParams): Observable<any> {
    const headers = {
      method: 'GET'
    };
    return this.http.get(this.baseUrl + route, { 
      headers,
      params,
      observe: 'response'
    }).pipe(
      map((res: HttpResponse<Object>) => {
        // do stuff
      }),
      catchError((err) => {
        console.log("error in api service get");
        return throwError(() => err);
      })
    );
  }

Then, there is a Foo service that has a number of functions that call this get method to get various resources. They are all structured like so:

 public getFoo1(): Observable<any[]> {
    return new Observable((observer) => {
      if (this._foo1) {
        observer.next(this._foo1);
        return observer.complete();
      }

      this.fetchFoo1()
        .pipe(
          catchError((err) => {
            console.log('Error in getFoo1'); // error handling ends here
            return throwError(() => err);
          })
        )
        .subscribe((res) => {
          this._foo1 = res;
          observer.next(this._foo1);
          observer.complete();
        });
    });
  }

  private fetchFoo1(nationalStandardId: number): Observable<Foo[]> {
    return this.api.get(`/foo`).pipe(
      map(res => res.foo),
      catchError(err => throwError(() => err))
    );
  }

Note that I'm attempting to catch and rethrow any errors from the get call. I call these methods in parallel via forkJoin:

    const dataCalls = forkJoin({
      getFoo1: this.fooService.getFoo1(),
      getFoo2: this.fooService.getFoo2(),
      getFoo3: this.fooService.getFoo3()
    }).pipe(
       catchError(err => console.log("error"))   // This is never called
    );

I've been testing error handling by not having my API running so all HTTP calls fail. The catchError operators in the fetchFoo functions execute, but the error handler in the outer catchError on the forkJoin is never called. Even if I put a pipe on the individual observables in the forkJoin, it doesn't work either:

    const dataCalls = forkJoin({
      getFoo1: this.fooService.getFoo1().pipe(
       catchError(err => console.log("error"))   // This is never called
    ),
      getFoo2: this.fooService.getFoo2().pipe(
       catchError(err => console.log("error"))   // This is never called
    ),
      getFoo3: this.fooService.getFoo3().pipe(
       catchError(err => console.log("error"))   // This is never called
    )
    });

Any idea what might be going on here?

(Note: This isn't how I'm actually implementing this. I'm having the fetchFoo methods return a fallback value, and that works just fine. I'm just trying to understand rxjs error handling better here)

Issue in Stackblitz: https://stackblitz.com/edit/angular-xps71aav?file=src%2Ffoo-service.service.ts


Solution

  • UPDATE:

    In the StackBlitz shared by Author, the error was not propagated from new Observable which caused the error to not reach the component level; we need to use observer.error(error) to propagate the error.

      this.fetchFoo3Info()
        .pipe(
          catchError((err) => {
            console.log('Error in getFoo3');
            return throwError(() => err);
          })
        )
        .subscribe({
          next: (res) => {
            this._foo3 = res;
            observer.next(this._foo3);
            observer.complete();
          },
          error: (error) => {
            observer.error(error);
          },
        });
    });
    

    Full Code:

    import { Injectable } from '@angular/core';
    import { catchError, map, Observable, throwError } from 'rxjs';
    import { ApiServiceService } from './api-service.service';
    
    @Injectable({
      providedIn: 'root',
    })
    export class FooServiceService {
      constructor(private api: ApiServiceService) {}
    
      private _foo1: any;
      private _foo2: any;
      private _foo3: any;
    
      public getFoo1(): Observable<any[]> {
        return new Observable((observer) => {
          if (this._foo1) {
            observer.next(this._foo1);
            return observer.complete();
          }
    
          this.fetchFoo1Info()
            .pipe(
              catchError((err) => {
                console.log('Error in getFoo1');
                return throwError(() => err);
              })
            )
            .subscribe({
              next: (res) => {
                this._foo1 = res;
                observer.next(this._foo1);
                observer.complete();
              },
              error: (error) => {
                observer.error(error);
              },
            });
        });
      }
    
      private fetchFoo1Info(): Observable<any[]> {
        return this.api.get('/foo1').pipe(
          map((res) => res),
          catchError((err) => {
            console.log('Foo1 error');
            return throwError(() => err);
          })
        );
      }
    
      public getFoo2(): Observable<any[]> {
        return new Observable((observer) => {
          if (this._foo2) {
            observer.next(this._foo2);
            return observer.complete();
          }
    
          this.fetchFoo2Info()
            .pipe(
              catchError((err) => {
                console.log('Error in getFoo2');
                return throwError(() => err);
              })
            )
            .subscribe({
              next: (res) => {
                this._foo2 = res;
                observer.next(this._foo2);
                observer.complete();
              },
              error: (error) => {
                observer.error(error);
              },
            });
        });
      }
    
      private fetchFoo2Info(): Observable<any[]> {
        return this.api.get('/foo2').pipe(
          map((res) => res),
          catchError((err) => {
            console.log('Foo2 error');
            return throwError(() => err);
          })
        );
      }
    
      public getFoo3(): Observable<any[]> {
        return new Observable((observer) => {
          if (this._foo3) {
            observer.next(this._foo3);
            return observer.complete();
          }
    
          this.fetchFoo3Info()
            .pipe(
              catchError((err) => {
                console.log('Error in getFoo3');
                return throwError(() => err);
              })
            )
            .subscribe({
              next: (res) => {
                this._foo3 = res;
                observer.next(this._foo3);
                observer.complete();
              },
              error: (error) => {
                throw new Error(error.message);
              },
            });
        });
      }
    
      private fetchFoo3Info(): Observable<any[]> {
        return this.api.get('/foo3').pipe(
          map((res) => res),
          catchError((err) => {
            console.log('Foo3 error');
            return throwError(() => err);
          })
        );
      }
    }
    

    Stackblitz Demo

    I think your code seems fine. You are rethrowing the errors using catchError(err => throwError(() => err)) which is correct.

    So the problem lies somewhere else.


    Try changing the API call to a normal GET call without the observe: 'response' since it could mess up the response.

    Also make sure the map operator is returning a value, since that can mess up the output.

      public get(route: string, params?: HttpParams): Observable<any> {
        const headers = {
          method: 'GET'
        };
        return this.http.get<any>(this.baseUrl + route, { 
          headers,
          params,
        }).pipe(
          map((res: HttpResponse<Object>) => {
            // do stuff
            res;
          }),
          catchError((err) => {
            console.log("error in api service get");
            return throwError(() => err);
          })
        );
      }
    

    Make sure you have subscribed to the final observable since it can cause the code to not execute. Since it was not present in the code, I am mentioning it here.

    const dataCalls = forkJoin({
      getFoo1: fetchFoo1(1),
      getFoo2: fetchFoo2(2),
      getFoo3: fetchFoo3(3),
    })
      .pipe(
        catchError((err: any) => console.log('error')) // This is never called
      )
      .subscribe({
        next: (data: any) => console.log(data, 'next'),
        error: (err: any) => console.log(err, 'error'),
        complete: () => console.log('complete'),
      });
    

    Below is a working example using plain RxJS to demonstrate that your code is correct.

    Full Code:

    import './style.css';
    
    import { catchError, throwError, map, Observable, forkJoin, of } from 'rxjs';
    
    const get = (route: string, params?: any): Observable<any> => {
      const headers = {
        method: 'GET',
      };
      return new Observable((subscriber: any) => {
        throw new Error('some error');
      }).pipe(
        map((res: any) => {
          // do stuff
          return res;
        }),
        catchError((err: any) => {
          console.log('error in api service get');
          return throwError(() => err);
        })
      );
    };
    
    const fetchFoo1 = (nationalStandardId: number): Observable<any[]> => {
      return get(`/foo1`).pipe(
        map((res: any) => res.foo),
        catchError((err: any) => throwError(() => err))
      );
    };
    
    const fetchFoo2 = (nationalStandardId: number): Observable<any[]> => {
      return get(`/foo2`).pipe(
        map((res: any) => res.foo),
        catchError((err: any) => throwError(() => err))
      );
    };
    
    const fetchFoo3 = (nationalStandardId: number): Observable<any[]> => {
      return get(`/foo3`).pipe(
        map((res: any) => res.foo),
        catchError((err: any) => throwError(() => err))
      );
    };
    const dataCalls = forkJoin({
      getFoo1: fetchFoo1(1),
      getFoo2: fetchFoo2(2),
      getFoo3: fetchFoo3(3),
    })
      .pipe(
        catchError((err: any) => console.log('error')) // This is never called
      )
      .subscribe({
        next: (data: any) => console.log(data, 'next'),
        error: (err: any) => console.log(err, 'error'),
        complete: () => console.log('complete'),
      });
    

    Stackblitz Demo