Rxjs Retry with Delay function
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)))
)
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)))
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
- rxjs-boost
- Original Source
- Spec File with Tests – probably comes in handy
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`);
}
}
});