53

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

Mujah Maskey
  • 8,654
  • 8
  • 40
  • 61

14 Answers14

111

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)))
)
Henrik
  • 9,714
  • 5
  • 53
  • 87
JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
  • 2
    the type of `errors` is `observable` and it doesn't have `complete` method – Mujah Maskey Jul 07 '17 at 21:00
  • 1
    I misunderstood the documentation. Should be OK now. See http://plnkr.co/edit/HTpcxkH9MqKqAsVep2na?p=preview – JB Nizet Jul 07 '17 at 21:22
  • I was getting "Supplied parameters do not match any signature of call target. on Observable.throw()" , so "Observable.throw('error') is working for me, and thanks a lot for your answer. – Mujah Maskey Jul 10 '17 at 13:33
  • This is not working for me. I keep getting `Property 'concat' does not exist on type 'Observable'.` even though I have imported concat, `import "rxjs/add/observable/concat";`! – Kyle Pfromer Dec 15 '17 at 04:38
  • @Kpfromer concat here is the instance method (operator). You want `import "rxjs/add/operator/concat";`. – JB Nizet Dec 15 '17 at 10:14
  • What should I pass through .throw(?) This method requires at last one argument. Usually I want the error from the subscriber. But how to access it? ... Is there another solution to dalay before retry? – Domske May 30 '18 at 15:45
  • The error on concat will be thrown before the original error, next or complete. This may cause issues. e.g. you get an error but the op was successfully. – Domske May 30 '18 at 16:18
  • 2
    Here's the complete code for newer Angular/RxJS versions: `pipe( retryWhen(error => error.pipe(delay(1000), take(3), concat(Observable.throw(error)))), catchError(myErrorHandler.bind(this)) )` – edwin Jun 29 '18 at 08:47
  • 3
    What to do if I want to stop retry for 401 error ? – Nikhil Mishra Mar 24 '21 at 09:36
  • 2
    The rethrow does not work properly. – hugo der hungrige Oct 21 '21 at 09:51
  • in rxjs 7 retryWhen is deprecated so have you any idea for equivalent code? – BASKA Jun 29 '23 at 09:38
26

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

David
  • 601
  • 7
  • 10
  • 1
    Any idea what the equivalent would be for `.concat(Observable.throw()))`? – Ed Bordin Apr 16 '18 at 03:09
  • @EdBordin if I was to guess I'd say `....pipe(delay(1000), take(5), concat(Observable.throw()))` – David Apr 18 '18 at 12:07
  • 1
    thanks! pretty close: `pipe(delay(1000), take(5), concat(Observable.throw('error message seems to be required')))` – Ed Bordin Apr 23 '18 at 05:23
  • 9
    @EdBordin @DavidTheProgrammer Any ideas how to get it right with RxJS 6 and the deprecated 'instance concat()'? I got `retryWhen(errors => concat(errors.pipe(delay(750), take(2)), throwError(errors))))` but it won't unwrap the original error. – Gerrit Jun 20 '18 at 15:39
  • Is there anyway I can get the error index as well ? I want to increase the delay for consequent errors. – Ashwin Surana Feb 22 '19 at 23:12
21

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

Adnan
  • 517
  • 3
  • 8
  • Something has to throw an error first before retry() gets hit right?, i think that retry function will only executive if something above throws an error – Kay Jul 04 '22 at 10:37
  • As retry() is an Error handling operator, it will be automatically triggered on error requests. So you don't have explicitly to throw an error to trigger it. Also here is an explanation on deprecated retryWhen() on their github: https://github.com/ReactiveX/rxjs/pull/6859 – Adnan Jul 04 '22 at 12:27
9

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

NiklasPor
  • 9,116
  • 1
  • 45
  • 47
  • Importing it from `'rxjs-boost/operators'` did not work. I had to import it from `'rxjs-boost/lib/operators'`. – Johannes Sep 30 '20 at 11:06
8

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 })
JojoIV
  • 992
  • 1
  • 9
  • 12
Eddy Lin
  • 513
  • 2
  • 6
  • Thanks a lot! I used this approach to implement progressive backoff `retryWhen(concatMap((err, index) => index < RETRY_COUNT ? timer(index * RETRY_PERIOD) : throwError(err)))` – developer Mar 14 '23 at 09:53
4

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`);
        }
      }
    });
Ricardo Saracino
  • 1,345
  • 2
  • 16
  • 37
  • I ha dsome type errors with this, but adjusting it like this, fixed it: `export function delayedRetry(delay: number, retries = 1): MonoTypeOperatorFunction { return (input) => input.pipe( retryWhen((errors) => { let retriesInner = 0; return errors.pipe( delayOperator(delay), map((error) => { if (retriesInner++ === retries) { throw error; } return error; }), ); }), ); }` – hugo der hungrige Oct 21 '21 at 10:17
3

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);
        })
      ))
    );
}
Stanislav Yermakov
  • 403
  • 1
  • 3
  • 8
3

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

}
Pradeep
  • 581
  • 3
  • 11
  • 19
3

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.

1

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) });
Yordan Nikolov
  • 2,598
  • 13
  • 16
1

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);
     })
  );
}
Johannes
  • 828
  • 12
  • 29
Jaywant Narwade
  • 145
  • 1
  • 4
1

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

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());
    }
  );
Mujah Maskey
  • 8,654
  • 8
  • 40
  • 61
0

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