1

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

3
  • This is a core concept of RxJS error handling and forkJoin. Once an error is caught inside a stream (Example: in a catchError), it is considered handled unless you rethrow it. Commented May 16 at 2:22
  • forkJoin will join multiple resources to one observable. As you are not subscribing or using async pipe to none of them nothing will be trigered. Commented May 16 at 13:42
  • @StefaniToto Of course I'm calling subscribe, I just thought it was so obvious that I didn't need to include it Commented May 16 at 20:26

2 Answers 2

1

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

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for the StackBlitz. I took out the HTTP call and the observe: response and did what you did in the StackBlitz, but I still can't hit the catchError on the forkJoin. I guess I'm going to have to dig a bit deeper to see what it is about my code that's making it bork
@MRichards could you replicate in an angular stacklblitz and see if you can replicate it?
Here is a Stackblitz - note that there is an additional layer of logic that I didn't include in my original question because I was trying to keep it simple (basically only executing the GET calls if the variables aren't hydrated). But you'll see in the app, that it's erroring out in the service class and not reaching the catchError on the forkJoin: stackblitz.com/edit/…
That's what I was missing. THANK YOU. It was driving me nuts, lol.
0

This is a core concept of RxJS error handling and forkJoin: Once an error is caught inside a stream (Example: in a catchError), it is considered handled unless you rethrow it.

Read this:

Current State

fetchFoo1:

return this.api.get(`/foo`).pipe(
  map(res => res.foo),
  catchError(err => throwError(() => err))  // Rethrows the error
);

Then:

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

The reason why catchError on forkJoin is not firing is that the error is caught and rethrown inside each fetchFooX() method, the forkJoin will terminate early on and you must subscribe to trigger the error chain properly. If you have not subscribed to dataCalls the observable chain will not execute at all.

Subscribe like this:

dataCalls.subscribe({
  next: data => {
    // Never reached in your test
  },
  error: err => {
    console.log("outer error", err);  // Now this should be called
  }
});

Full fixed version

The Code:

const dataCalls = forkJoin({
  getFoo1: this.fooService.fetchFoo1(),
  getFoo2: this.fooService.fetchFoo2(),
  getFoo3: this.fooService.fetchFoo3()
});

dataCalls.pipe(
  catchError(err => {
    console.log("forkJoin error", err);
    return throwError(() => err); // Optional: If you want upstream to handle it too
  })
).subscribe({
  next: res => {
    console.log("All results", res);
  },
  error: err => {
    console.log("subscription error handler", err);
  }
});

1 Comment

Thanks - I have seen that this works in a StackBlitz, but it still doesn't execute as expected in my code. I guess I have to dig a little deeper to figure it out

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.