Mujah Maskey
Mujah Maskey

Reputation: 8804

Rxjs Retry with Delay function

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

Upvotes: 59

Views: 79961

Answers (15)

Alberto Basalo
Alberto Basalo

Reputation: 273

Finally, I figured out how to filter some errors, wait to retry, and limit the retries.

The time-out could be easily incremental.

import {
  HttpErrorResponse,
  HttpEvent,
  HttpHandlerFn,
  HttpRequest,
} from '@angular/common/http';
import { Observable, retry, throwError, timer } from 'rxjs';

const count = 2;
const timeout = 1000;
const resetOnSuccess = false;

function delay(error: HttpErrorResponse, retries: number): Observable<number> {
  const canRetryStatus = error.status === 0 || error.status >= 500;
  const canRetryCounter = retries < count;
  const shouldRetry = canRetryStatus && canRetryCounter;
  return shouldRetry ? timer(timeout) : throwError(() => error);
}

export function ErrorInterceptor(
  req: HttpRequest<unknown>,
  next: HttpHandlerFn
): Observable<HttpEvent<unknown>> {
  return next(req).pipe(retry({ count, delay, resetOnSuccess }));
}

Upvotes: 0

Eddy Lin
Eddy Lin

Reputation: 683

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 })

Upvotes: 20

Adnan
Adnan

Reputation: 717

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().

Upvotes: 35

Sanjay Garg
Sanjay Garg

Reputation: 9

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"])))
    }

Upvotes: 0

Andres Kiik
Andres Kiik

Reputation: 56

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))
    )
  ))
);

Upvotes: 1

Ricardo Saracino
Ricardo Saracino

Reputation: 1420

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`);
        }
      }
    });

Upvotes: 4

Jaywant Narwade
Jaywant Narwade

Reputation: 165

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);
     })
  );
}

Upvotes: 1

Pradeep
Pradeep

Reputation: 641

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);
    }

}

Upvotes: 3

NiklasPor
NiklasPor

Reputation: 9815

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

Upvotes: 9

Fran&#231;ois Menet
Fran&#231;ois Menet

Reputation: 71

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.

Upvotes: 3

JB Nizet
JB Nizet

Reputation: 692181

delay() is used to introduce a delay between events emitted by the observable. But the observable never emits any event. It just errors immediately.

What you're looking for is retryWhen(), which allows deciding after how long to retry:

RxJS 5:

  .retryWhen(errors => errors.delay(1000).take(10))

RxJS 6:

import { retryWhen, delay, take } from 'rxjs/operators'
someFunction().pipe(
  // ...
  retryWhen(errors => errors.pipe(delay(1000), take(10)))
)

This will complete the whole observable after 10 attempts. If you want to error the whole observable after 10 attempts, the observable returned by the retryWhen callback must throw:

RxJS 5:

  .retryWhen(errors => errors.delay(1000).take(10).concat(Observable.throw()))

RxJS 6:

import { retryWhen, delay, take, concatMap, throwError } from 'rxjs/operators'
someFunction().pipe(
  // ...
  retryWhen(errors => errors.pipe(delay(1000), take(10), concatMap(throwError)))
)

Upvotes: 117

Stanislav Yermakov
Stanislav Yermakov

Reputation: 403

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);
        })
      ))
    );
}

Upvotes: 3

David
David

Reputation: 816

To add on to @JB Nizet's answer. If you're writing this in rxjs 5+ with lettable operators, structure it as

retryWhen(errors => errors.pipe(delay(1000), take(5)))

Upvotes: 27

Mujah Maskey
Mujah Maskey

Reputation: 8804

I come up with following solution using retryWhen and Observable.Interval, but in this solution, error function of subscribe never calls,

this.branchService.getCanCreate()
  .do((value) => {
    this.cCreate = value;
  }, (error) => {
    console.log('do', error + new Date().toTimeString());
  })
  .retryWhen(errors => {
    return Observable.interval(1000).take(3).concat(Observable.throw('error')));
  })
  .subscribe(
    value => {
      this.cCreate = !!value
      console.log('success', new Date().toTimeString());
    },
    error => {
      console.log('subscribe', error + new Date().toTimeString());
      this.cCreate = false;
    },
    () => {
      console.log('finally', new Date().toTimeString());
    }
  );

Upvotes: 0

Yordan Nikolov
Yordan Nikolov

Reputation: 2678

This may help you

let values$ = Rx.Observable.interval(1000).take(5);
let errorFixed = false;

values$
.map((val) => {
   if(errorFixed) { return val; }
   else if( val > 0 && val % 2 === 0) {
      errorFixed = true;
      throw { error : 'error' };

   } else {
      return val;
   }
})
.retryWhen((err) => {
    console.log('retrying again');
    return err.delay(1000).take(3); // 3 times
})
.subscribe((val) => { console.log('value',val) });

Upvotes: 1

Related Questions