LiveLoveApp logo

Solution - RetryWhen Operator

Solution

import { fromEvent, of, throwError } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import {
  catchError,
  finalize,
  map,
  mergeMap,
  retryWhen
} from 'rxjs/operators';

interface UserResponse {
  data: {
    id: number;
    email: string;
    first_name: string;
    last_name: string;
    avatar: string;
  };
}

function random(max: number, min: number): number {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

const output = document.getElementById('output') as HTMLTextAreaElement;
const btn = document.getElementById('btn') as HTMLButtonElement;

fromEvent(btn, 'click')
  .pipe(
    map(() => random(10, 15)),
    mergeMap((id) =>
      ajax.getJSON<UserResponse>(`https://reqres.in/api/users/${id}`).pipe(
        map((response) => response.data),
        catchError((error) => {
          output.value += `\n\n${error.message}`;
          output.scrollTop = output.scrollHeight;
          return throwError(error);
        }),
        retryWhen((notifier) =>
          notifier.pipe(
            mergeMap((error, i) => {
              // retry maximum of 2 times when the status code is 404
              const MAX_RETRIES = 2;
              if (i < MAX_RETRIES) {
                if (error.status === 404) {
                  return of(null);
                }
              }
              return throwError(error);
            })
          )
        )
      )
    ),
    finalize(() => {
      btn.classList.add('cursor-not-allowed');
      btn.classList.add('opacity-50');
    })
  )
  .subscribe({
    error: (e) => console.error('observer', e),
    next: (value) => {
      output.value += `\n\n${JSON.stringify(value, null, 2)}`;
      output.scrollTop = output.scrollHeight;
    },
    complete: () => console.log('complete')
  });

See example on codesandbox

Let's focus on the retryWhen() operator in the code solution above.

  • The retryWhen() operator is going to retry the source Observable, which in this case, is the Observable created by the ajax.getJSON() method.
  • When the source Observable emits an error notification, the retryWhen() will invoke the callback function and provider the notifier Observable. The notifier Observable emits a next notification each time the source Observable emits an error notification.
  • We use the mergeMap() operator on the notifier Observable in order to merge the result of the inner Observables, which will either be an Observable created by the of() operator that will immediately emit a null next notification, or the Observable created by the throwError() operator that will immediately emit an error notification.
  • Within the mergeMap() we set a MAX_RETRIES constant to the number 2. This will prevent endless retries. The mergeMap() operator's projection function is invoked with the value emitted from the source notifier Observable and the index. The index is a count of the number of next notification the source Observable has emitted. We can use the index argument to track how many times Observable created by the ajax.getJSON() has emitted an error notification.
  • If the index, which is i in our example, is less than MAX_RETRIES we check the error's status code and return an Observable using the of() operator, which immediately emits the next notification value of null.
  • Else, we return a new Observable created by the throwError() Observable that immediately emits an error notification. The retryWhen() operator subsequently invokes the error() method on the source Observable.