Search code examples
angularrxjsobservablesubscriptionangular-httpclient

Error handling inside RxJS mergeMap operator


When I make a GET request using the Angular HttpClient, I get an observable back and process it within the RxJS operator mergeMap.

Now it happens that again and again a 404 is thrown, which I would like to catch. Finally, no error message should appear in the browser console and the pipe should be processed with the next value of the stream.

Is there a possibility for that? I did not manage it with catchError().

Here is a simplified version of my code:

    ...
    this.service1.getSomeStuff().pipe(
          mergeMap((someStuff) => {
            return from(stuff);
          }),
          mergeMap((stuff) => {
            return this.service2.getMoreStuff(stuff.id); // Here I need some error handling, if 404 occurs
          }),
          mergeMap((things) => {
            return from(things).pipe(
              mergeMap((thing) => {
                if (allLocations.some(x => x.id === metaData.id)) {
                  return this.service2.getMore(thing.id, thing.type, thing.img_ref);
                }
              }),
              map((thing) => {
              ...

UPDATE: Added approach with catchError()

I tried it that way, but the error is not detected and the next mergeMap does not work (IDE does not recognize parameter like thing.id, thing.type, thing.img_ref anymore):

...
this.service1.getSomeStuff().pipe(
      mergeMap((someStuff) => {
        return from(stuff);
      }),
      mergeMap((stuff) => {
        return this.service2.getMoreStuff(stuff.id).pipe(
          catchError(val => of(`Error`))
        );
      }),
      mergeMap((things) => {
        return from(things).pipe(
          mergeMap((thing) => {
            if (allLocations.some(x => x.id === metaData.id)) {
              return this.service2.getMore(thing.id, thing.type, thing.img_ref);
            }
          }),
          map((thing) => {
          ...

Solution

  • You'll need to use retry or retryWhen (names are pretty self-explanatory) — these operators will retry a failed subscription (resubscribe to the source observable, once an error is emitted.

    To raise the id upon each retry — you could lock it in a scope, like this:

    const { throwError, of, timer } = rxjs;
    const { tap, retry, switchMap } = rxjs.operators;
    
    console.log('starting...');
    
    getDetails(0)
      .subscribe(console.log);
    
    
    function getDetails(id){
      // retries will restart here
      return of('').pipe(
        switchMap(() => mockHttpGet(id).pipe(
          // upon error occurence -- raise the id
          tap({ error(err){
            id++;
            console.log(err);
          }})
        )),  
        retry(5) // just limiting the number of retries
                 // you could go limitless with `retry()`
      )
    }
    
    function mockHttpGet(id){
      return timer(500).pipe(
        switchMap(()=>
          id >= 3
          ? of('success: ' + id)
          : throwError('failed for ' + id)
        )
      );
    }
    <script src="https://unpkg.com/rxjs@6.4.0/bundles/rxjs.umd.min.js"></script>

    Please, note that it would be wiser to have a conditional retry to retry only on 404 errors. That could be achieved via retryWhen, e.g.

    // pseudocode
    retryWhen(errors$ => errors$.pipe(filter(err => err.status === '404')))
    

    Check this article on error handling in rxjs to get more affluent with retry and retryWhen.

    Hope this helps


    UPDATE: there are also other ways to achieve that:

    const { throwError, of, timer, EMPTY } = rxjs;
    const { switchMap, concatMap, map, catchError, take } = rxjs.operators;
    
    console.log('starting...');
    
    getDetails(0)
      .subscribe(console.log);
    
    
    function getDetails(id){
      // make an infinite stream of retries
      return timer(0, 0).pipe(
        map(x => x + id),
        concatMap(newId => mockHttpGet(newId).pipe(
          // upon error occurence -- suppress it
          catchError(err => {
            console.log(err);
            // TODO: ensure its 404
    
            // we return EMPTY, to continue
            // with the next timer tick
            return EMPTY;
          })
        )),
        // we'll be fine with first passed success
        take(1)
      )
    }
    
    function mockHttpGet(id){
      return timer(500).pipe(
        switchMap(()=>
          id >= 3
          ? of('success: ' + id)
          : throwError('failed for ' + id)
        )
      );
    }
    <script src="https://unpkg.com/rxjs@6.4.0/bundles/rxjs.umd.min.js"></script>