Rxjs Retry with Delay function

Viewed 57025

I am trying to use retry with delay function, I expect function will call after 1000ms delay, but it doesnot, what can be error here? look at console output, it is same time 16:22:48.

I expect there 16:22:48, 16:22:59 ...

canCreate: boolean;
getSomeFunction(): Observable<boolean> {
        return new Observable<boolean>(
            observer => {
                const canCreate = null; // this is just null for now, will some value later
                if (canCreate == null) {
                    observer.error('error');
                } else {
                    observer.next(true);
                }
                observer.complete();
            }
        )
    }


this.getSomeFunction()
      .do((value) => {
        this.cCreate = value;
      }, (error) => {
         console.log(error + new Date().toTimeString());
      })
      .delay(1000)
      .retry(10)
      .subscribe(
        value => this.cCreate = value,
        error => {
          this.cCreate = false;
        },
        () => {}
      );
  }

and console result is :

enter image description here

14 Answers

All of this is RxJS 6+


TL;DR

You could use the fully tested operator from this package, or scroll down to see the source :)

npm i rxjs-boost
import { retryWithDelay } from 'rxjs-boost/operators';

obs$.pipe(
  // will retry 4 times with a 1s delay before each try:
  retryWithDelay(1000, 4)
);

Criteria

As most (or maybe none) of the other answer didn't meet all of my criteria, I'll list my solution below. Goals:

  • Emits & completes regularly if no error is thrown. ✅
  • Retries x times if an error is thrown. ✅
  • Has a delay of y before each retry. ✅
  • Returns the last emitted error. (A lot of other answers were struggling with this.) ✅
  • Correct typing with strict: true – but this was quite difficult to mess up. ✅

Solution

As every other answer we'll use the retryWhen operator to catch the errors. To track the amount of repetitions can use the scan operator. To limit the amount of repetitions we'll simply throw an error inside a map operator.

The original source uses throwIf, but in that case we could simply use retryWithDelay from rxjs-boost.

Last we'll use the delay operator to add the delay between the different executions:

import { MonoTypeOperatorFunction } from 'rxjs';
import { delay as delayOperator, map, retryWhen, scan } from 'rxjs/operators';

export function retryWithDelay<T>(
  delay: number,
  count = 1
): MonoTypeOperatorFunction<T> {
  return (input) =>
    input.pipe(
      retryWhen((errors) =>
        errors.pipe(
          scan((acc, error) => ({ count: acc.count + 1, error }), {
            count: 0,
            error: undefined as any,
          }),
          map((current) => {
            if (current.count > count) {
              throw current.error;
            }
            return current;
          }),
          delayOperator(delay)
        )
      )
    );
}

Sources

As the accepted answer's suggestion to use retryWhen() was ok at that time, now, as we can see on the provided link for retryWhen(), this operator is deprecated and will be removed as from the RxJS version 9 or 10. So all solutions with retryWhen() are deprecated as well.

Now a solution that is suggested by the RxJS dev team and the docs is to use retry() operator, which is also an error handling operator, with the retryConfig param.

Now, to achieve the same logic, with less code, you would use the following:

// A custom method to check should retry a request or not
shouldRetry(error: HttpErrorResponse) {
    // Example for catching specific error code as well
    if (error.status === 503) {
      return timer(1000); // Adding a timer from RxJS to return observable to delay param.
    }

    throw error;
  }

// Retry operator somewhere in your code
retry({ count: 2, delay: this.shouldRetry })

As you could see from the example above, we have used less code and less RxJS operators to achieve some logic for which we needed a lot more using the retryWhen() operator. This is also the case why the team is deprecating this operator, as the same logic can be achieved using the retry() with this config.

Note: you would have to adopt the code I provided as per your needs, but this is an example of how to use the retry() operator instead of retryWhen().

I came to this conclusion, in order to retry with other operations in the http pipe

import {delay as _delay, map, retryWhen} from 'rxjs/operators';

export const delayedRetry = (delay, retries = 1) => retryWhen(result => {
    let _retries = 0;
    return result.pipe(
      _delay(delay),
      map(error => {
        if (_retries++ === retries) {
          throw error;
        }
        return error;
      }),
    );
  },
);

Usage

    http.pipe(
      delayedRetry(1500, 2),
      catchError((err) => {
        this.toasterService.error($localize`:@@not-saved:Could not save`);
        return of(false);
      }),
      finalize(() => this.sending = false),
    ).subscribe((success: boolean) => {
        if (success === true) {
           this.toasterService.success($localize`:@@saved:Saved`);
        }
      }
    });

For ngrx5+ we could create operator:


function retryRequest(constructor: () => Observable, count: number, delayTime: number) {
  let index = 0;
  return of(1) // we need to repeat not the result of constructor(), but the call of constructor() itself
    .pipe(
      switchMap(constructor),
      retryWhen(errors => errors.pipe(
        delay(delayTime),
        mergeMap(error => {
          if (++index > count) {
            return throwError(error);
          }
          return of(error);
        })
      ))
    );
}

Works on rxjs version 6.3.3

https://stackblitz.com/edit/http-basics-8swzpy

Open Console and see the retries

Sample Code

import { map, catchError, retryWhen, take, delay, concat } from 'rxjs/operators';
import { throwError } from 'rxjs';


export class ApiEXT {

    static get apiURL(): string { return 'http://localhost:57886/api'; };
    static httpCLIENT: HttpClient;

 static POST(postOBJ: any, retryCOUNT: number = 0, retryINTERVAL: number = 1000) {
        return this.httpCLIENT
            .post(this.apiURL, JSON.stringify(postOBJ))
            .pipe(
                map(this.handleSUCCESS),
                retryWhen(errors => errors.pipe(delay(retryINTERVAL), take(retryCOUNT), concat(throwError("Giving up Retry.!")))),
                catchError(this.handleERROR));
    }


  private static handleSUCCESS(json_response: string): any {
        //TODO: cast_and_return    
        return JSON.parse(json_response);

    }

 private static handleERROR(error: Response) {
        let errorMSG: string;
        switch (error.status) {
            case -1: errorMSG = "(" + error.status + "/" + error.statusText + ")" + " Server Not Reachable.!"; break;
            default: errorMSG = "(" + error.status + "/" + error.statusText + ")" + " Unknown Error while connecting with server.!"; break;
        }
        console.error(errorMSG);
        return throwError(errorMSG);
    }

}

I recently had this problem, and found that the accepted solution could be improved.

Observable.pipe(
     retryWhen(errors => errors.pipe(
      delay(1000),
      take(10))),
    first(v => true),
    timeout(10000))

What it essentially does is to retry as mentioned, but this finishes right away without adding any (erroneous) value using the 'first' operator.

If it cannot find a value within the timeout timeframe, an error is raised.

RxJS provides retry operator that resubscribes the Observable for the given number of count when there is an error. Before throwing error Observable is resubscribed for the given number of count by retry operator and if still there is an error, then error is thrown. retry is useful to hit the URL many times. It is possible that because of network bandwidth, URL does not return successful data in one time and when it reties, it may return data successfully. If after retying still there is error in Observable then catchError can be used to return Observable with user defined default data.

getBook(id: number): Observable<Book> {
  return this.http.get<Book>(this.bookUrl + "/" + id).pipe(
     retry(3),
     catchError(err => {
      console.log(err);
      return of(null);
     })
  );
}

As accepted answer rethrow didn't work for me, I ended up with this:

Retry 3 times with 2 seconds delay and if no success throw an error.

Observable.pipe(
  retryWhen(errors => errors.pipe(
    mergeMap((error, i) =>
      iif(() => i >= 3, throwError(() => error), timer(2000))
    )
  ))
);

1. Using Concat

getPosts()
      {
        return this.httpClient
        .get<any[]>('https://jsonplaceholder.typicode.com/postss')
        .pipe(
          retryWhen(errors =>{
            return concat(
              errors.pipe(
                delay(2000),
                take(2),
              ),
              throwError("Max Retries Exceeded!")
            )
          }),
          catchError(()=>of(["Angular","Rxjs"])))
      }

2. Using ConcatMap

return this.httpClient
    .get<any[]>('https://jsonplaceholder.typicode.com/postss')
    .pipe(
      retryWhen(errors =>{
        return errors.pipe(
          concatMap((error,index)=> {
            if(index>=2) return throwError(error);
            else return of(error).pipe(delay(2000))
          })
        )
      }),
      catchError(()=>of("Angular","Rxjs"])))

3. Using Scan

return this.httpClient
    .get<any[]>('https://jsonplaceholder.typicode.com/postss')
    .pipe(
      retryWhen(errors =>{
        return errors.pipe(
         scan( (acc, error) => {
           if(acc>2) throw error;
           return acc+1;
         }, 1),
          delayWhen(val => timer(val * 1000)),
        )
      }),
      catchError(()=>of("Angular","Rxjs"])))
    }

Retry 5 times with a delay of 500ms:

RxJS 6.x

  • Use index to count
  • Transform retryWhen 's error Observable to timer or throwError
retryWhen(concatMap((err, index) => index < 5 ? timer(500) : throwError(err)))

RxJS 6.3 ⬆

  • Use index to count
  • In retryWhen 's error Observable, use delayWhen to wait timer or throwError
retryWhen(delayWhen((err, index) => index < 5 ? timer(500) : throwError(err)))

or you can do this :

const delayFn = (delay = 0, count = Infinity) => {
    return (err, index) => index < count ? timer(delay) : throwError(err);
};
retryWhen(concatMap(delayFn(500, 5))) // RxJS 6.x
retryWhen(delayWhen(delayFn(500, 5))) // RxJS 6.3 ⬆

RxJS 7.3 ⬆

  • Just retry
retry({ count: 5, delay: 500 })
Related