5

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) => {
          ...
Codehan25
  • 2,704
  • 10
  • 47
  • 94
  • catch error should capture the error requests – Sachila Ranawaka Apr 17 '19 at 12:37
  • `return this.service2.getMoreStuff(stuff.id).pipe(catchError())` – Roberto Zvjerković Apr 17 '19 at 12:38
  • @SachilaRanawaka Unfortunately, it does not work that way either. Updated my question and posted my approach with catchError. – Codehan25 Apr 17 '19 at 12:46
  • @ritaj Does not work like that.. – Codehan25 Apr 17 '19 at 12:46
  • @Codehan25, is your intention to retry a http-get if an error happens? – kos Apr 17 '19 at 12:53
  • @Kos No, the pipe should simply be processed with the next value or the method getMoreStuff(stuff.id) should be called with the next stuff.id from the stream. Sometimes a 404 is thrown for getMoreStuff (stuff.id) because nothing is returned for that particular stuff.id. – Codehan25 Apr 17 '19 at 13:00
  • So you want to suppress the http-get error and wait for the next value on the stream? Then simply `catchError(() => NEVER)`. If you want to suppress the error and pass the **current** value upstream — then `catchError(() => of(stuff))`. Otherwise, please rephrase your question. – kos Apr 17 '19 at 13:05
  • Well, finally, the call to the function getMoreStuff(stuff.id) should just be repeated with getMoreStuff(stuff.id + 1) when a 404 occurs. Would not know how else to formulate my question.. – Codehan25 Apr 17 '19 at 13:16

1 Answers1

5

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>
kos
  • 5,044
  • 1
  • 17
  • 35
  • This looks like the right approach, but what can I do if my id is a string? In my example, stuff.id is a string. How can I get net net stuff.id from the current stream? If I can get this, then my problem is solved. – Codehan25 Apr 17 '19 at 14:21
  • @Codehan25 well, just convert it back-n-forth: `parseInt(aString, 10)` to parse and `42.toString()` if you need it back as a string – kos Apr 17 '19 at 14:24
  • But that parsed my id and counts her up. I need the next id from the stream. This somehow messes me up now .. – Codehan25 Apr 17 '19 at 14:29
  • @Codehan25, alas, I don't get your particular case. If you have a source stream of ids, e.g. `from([1, 2, 3])`, then just suppress an error with `catchError(() => NEVER)` and wait for the next id. – kos Apr 17 '19 at 14:38
  • But with catch error I have to return an observable, otherwise the operator will not work. – Codehan25 Apr 17 '19 at 14:47
  • @Codehan25, [NEVER](https://rxjs.dev/api/index/const/NEVER) is an Observable, see the docs. `import {NEVER} from 'rxjs';` to use it – kos Apr 17 '19 at 14:49
  • 1
    I automatically imported it in lowercase letters (never). After I imported it in capital letters it worked. Thank you for your help. I think that with this approach I'll reach my goal. – Codehan25 Apr 17 '19 at 14:56
  • More about retry and retyrWhen use-cases you can read in my articles: https://dev.to/oleksandr/rxjs-operators-retry-vs-repeat-5gn and https://medium.com/@alexanderposhtaruk/rx-js-replywhen-use-case-in-iframe-d-angular-spa-d86d35d3bce8 – Oleksandr Poshtaruk Apr 27 '19 at 06:31