Skip to main content

retry or fallback mechanisms with error handling

To implement retry or fallback mechanisms with error handling in Angular, you can use RxJS operators like retry, retryWhen, and catchError. These operators allow you to handle errors gracefully and retry failed HTTP requests or provide fallback values.


1. Using retry for Automatic Retries

The retry operator retries a failed HTTP request a specified number of times before throwing an error.

  • Description: Automatically retries the observable a specified number of times when an error occurs.
  • Use Case: Use retry when you want to retry a failed observable a fixed number of times without any custom logic.
  • Syntax:
    retry(count: number): MonoTypeOperatorFunction<T>
    • count: The number of retry attempts.

Example

import { of } from 'rxjs';
import { map, retry } from 'rxjs/operators';

let attempt = 0;

of('data')
.pipe(
map(() => {
attempt++;
if (attempt < 3) {
throw new Error('Error occurred');
}
return 'Success';
}),
retry(2) // Retry up to 2 times
)
.subscribe({
next: (value) => console.log(value),
error: (err) => console.error('Final Error:', err.message),
});

Output

Error occurred
Error occurred
Success
  • Explanation:
    • The observable retries twice after the first error.
    • On the third attempt, it succeeds and emits "Success".
    • If all retries fail, the error is propagated.

Realtime Example:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { retry, catchError } from 'rxjs/operators';

@Injectable({
providedIn: 'root',
})
export class DataService {
private apiUrl = 'https://api.example.com/data';

constructor(private http: HttpClient) {}

getData(): Observable<any> {
return this.http.get(this.apiUrl).pipe(
retry(3), // Retry up to 3 times
catchError((error) => {
console.error('Error occurred:', error);
throw error; // Re-throw the error after retries
})
);
}
}

2. Using retryWhen for Conditional Retries

The retryWhen operator allows you to retry based on custom logic, such as waiting for a delay before retrying.

  • Description: Retries the observable based on a custom logic defined in a notifier function. The notifier function receives an observable of errors and returns an observable that determines when to retry or terminate.
  • Use Case: Use retryWhen when you need custom retry logic, such as adding delays, exponential backoff, or stopping retries based on specific conditions.
  • Syntax:
    retryWhen(notifier: (errors: Observable<any>) => Observable<any>): MonoTypeOperatorFunction<T>

Example

import { of, timer } from 'rxjs';
import { map, retryWhen, delayWhen } from 'rxjs/operators';

let attempt = 0;

of('data')
.pipe(
map(() => {
attempt++;
if (attempt < 3) {
throw new Error('Error occurred');
}
return 'Success';
}),
retryWhen((errors) =>
errors.pipe(
delayWhen(() => timer(1000)) // Wait 1 second before retrying
)
)
)
.subscribe({
next: (value) => console.log(value),
error: (err) => console.error('Final Error:', err.message),
});

Output

Error occurred (after 1 second)
Error occurred (after 1 second)
Success
  • Explanation:
    • The retryWhen operator uses the errors observable to determine when to retry.
    • In this example, it waits 1 second before each retry.
    • On the third attempt, it succeeds and emits "Success".

Realtime Example:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, throwError, timer } from 'rxjs';
import { retryWhen, catchError, delayWhen } from 'rxjs/operators';

@Injectable({
providedIn: 'root',
})
export class DataService {
private apiUrl = 'https://api.example.com/data';

constructor(private http: HttpClient) {}

getData(): Observable<any> {
return this.http.get(this.apiUrl).pipe(
retryWhen((errors) =>
errors.pipe(
delayWhen(() => timer(2000)), // Wait 2 seconds before retrying
catchError((error) => {
console.error('Retries exhausted:', error);
return throwError(() => new Error('Failed after retries'));
})
)
),
catchError((error) => {
console.error('Error occurred:', error);
throw error;
})
);
}
}

3. Providing a Fallback Value

The catchError operator can be used to provide a fallback value when an error occurs.

Example:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable({
providedIn: 'root',
})
export class DataService {
private apiUrl = 'https://api.example.com/data';

constructor(private http: HttpClient) {}

getData(): Observable<any> {
return this.http.get(this.apiUrl).pipe(
catchError((error) => {
console.error('Error occurred:', error);
return of({ fallbackData: true }); // Provide fallback data
})
);
}
}

4. Combining Retry and Fallback

You can combine retry and catchError to retry a request and provide fallback data if all retries fail.

Example:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { retry, catchError } from 'rxjs/operators';

@Injectable({
providedIn: 'root',
})
export class DataService {
private apiUrl = 'https://api.example.com/data';

constructor(private http: HttpClient) {}

getData(): Observable<any> {
return this.http.get(this.apiUrl).pipe(
retry(3), // Retry up to 3 times
catchError((error) => {
console.error('Error occurred after retries:', error);
return of({ fallbackData: true }); // Provide fallback data
})
);
}
}

5. Using finalize for Cleanup

The finalize operator can be used to perform cleanup actions, such as hiding a loading spinner, regardless of whether the request succeeds or fails.

Example:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { retry, catchError, finalize } from 'rxjs/operators';

@Injectable({
providedIn: 'root',
})
export class DataService {
private apiUrl = 'https://api.example.com/data';

constructor(private http: HttpClient) {}

getData(): Observable<any> {
console.log('Loading started');
return this.http.get(this.apiUrl).pipe(
retry(3),
catchError((error) => {
console.error('Error occurred:', error);
throw error;
}),
finalize(() => {
console.log('Loading finished'); // Cleanup action
})
);
}
}

6. Example with Multiple HTTP Calls

If you have multiple dependent HTTP calls, you can use switchMap or mergeMap with retry and fallback mechanisms.

Example:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { switchMap, retry, catchError } from 'rxjs/operators';

@Injectable({
providedIn: 'root',
})
export class DataService {
private apiUrl1 = 'https://api.example.com/data1';
private apiUrl2 = 'https://api.example.com/data2';

constructor(private http: HttpClient) {}

getData(): Observable<any> {
return this.http.get(this.apiUrl1).pipe(
retry(3),
switchMap((response1: any) => {
const dependentId = response1.id;
return this.http.get(`${this.apiUrl2}/${dependentId}`);
}),
catchError((error) => {
console.error('Error occurred:', error);
return of({ fallbackData: true });
})
);
}
}

7. Summary

  • Retry Mechanisms:
    • Use retry for simple retries.
    • Use retryWhen for conditional retries with delays.
  • Fallback Mechanisms:
    • Use catchError to provide fallback data or handle errors gracefully.
  • Cleanup:
    • Use finalize to perform cleanup actions like hiding a loading spinner.
  • Chaining HTTP Calls:
    • Use switchMap or mergeMap for dependent HTTP calls with retry and fallback.

This approach ensures robust error handling and improves the reliability of your Angular application.