31

I am trying to make an API call (using angular4), which retries when it fails, using retryWhen. I want it to delay for 500 ms and retry again. This can be achieved with this code:

loadSomething(): Observable<SomeInterface> {
  return this.http.get(this.someEndpoint, commonHttpHeaders())
    .retryWhen(errors => errors.delay(500));
}

But this will keep trying forever. How do I limit it to, let's say 10 times?

Thank you!

Mouneer
  • 12,827
  • 2
  • 35
  • 45
Tomer Almog
  • 3,604
  • 3
  • 30
  • 36
  • Here is another way to implement it: https://stackoverflow.com/questions/39928183/angular-2-rxjs-observable-retrywhen-filter-retry-on-error-status/41720854#41720854 – Radu Cojocari Jul 05 '17 at 12:41

2 Answers2

88

You need to apply the limit to the retry signal, for instance if you only wanted 10 retries:

loadSomething(): Observable<SomeInterface> {
  return this.http.get(this.someEndpoint, commonHttpHeaders())
    .retryWhen(errors => 
      // Time shift the retry
      errors.delay(500)
            // Only take 10 items
            .take(10)
            // Throw an exception to signal that the error needs to be propagated
            .concat(Rx.Observable.throw(new Error('Retry limit exceeded!'))
    );

EDIT

Some of the commenters asked how to make sure that the last error is the one that gets thrown. The answer is a bit less clean but no less powerful, just use one of the flattening map operators (concatMap, mergeMap, switchMap) to check which index you are at.

Note: Using the new RxJS 6 pipe syntax for future proofing (this is also available in later versions of RxJS 5).

loadSomething(): Observable<SomeInterface> {
  const retryPipeline = 
    // Still using retryWhen to handle errors
    retryWhen(errors => errors.pipe(
      // Use concat map to keep the errors in order and make sure they
      // aren't executed in parallel
      concatMap((e, i) => 
        // Executes a conditional Observable depending on the result
        // of the first argument
        iif(
          () => i > 10,
          // If the condition is true we throw the error (the last error)
          throwError(e),
          // Otherwise we pipe this back into our stream and delay the retry
          of(e).pipe(delay(500)) 
        )
      ) 
  ));

  return this.http.get(this.someEndpoint, commonHttpHeaders())
    // With the new syntax you can now share this pipeline between uses
    .pipe(retryPipeline)
}
Kamil Kiełczewski
  • 85,173
  • 29
  • 368
  • 345
paulpdaniels
  • 18,395
  • 2
  • 51
  • 55
  • 3
    How would you make the final error be the last error thrown? – felixfbecker Oct 03 '17 at 08:03
  • 1
    Caution: The error on concat will thrown before the original observable error is thrown. The method is called one last time. This may cause issues. – Domske May 30 '18 at 16:14
  • @Dominik Perhaps you have since figured this out, but I updated my answer from your comments. – paulpdaniels Aug 14 '18 at 04:47
  • 2
    Another word of caution, we made some tests with a colleague and noticed that the throwError is called always. So to understand this we came to another post: https://stackoverflow.com/questions/54097971/rxjs-iif-arguments-are-called-when-shouldnt TLDR: "the role of iif is not to execute one path over the other, but to subscribe to one Observable or the other" – JSantaCL Nov 04 '19 at 21:03
  • @JSantaCL that is correct, though if you really want suspended side effects you can just wrap the clause in `defer` – paulpdaniels Nov 06 '19 at 00:45
  • I was trying to achievement something similar: retries with delay, and throw error only on the last retry. This works for me: `.retryWhen( errors => errors.pipe( delay(2000), take(3), concatMap( (e, i) => ( i < 2 ? of(e) : throwError(e) ) ) ) )`. I tried with the `iff` but it didn't work for me. :( – David Ferreira Jan 09 '21 at 19:36
-13

Use

.retry(3)

Repeats the source observable sequence the specified number of times or until it successfully terminates.

https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/retry.md