Skip to content

retry

Imagine you're trying to do something that might fail occasionally due to temporary issues (like a shaky internet connection). The retry operator helps you automatically try that operation again if it fails.

It subscribes to the original (source) Observable. If that Observable emits an error, instead of immediately passing the error down the chain, retry resubscribes to the source Observable, effectively "trying again".

Key Points#

  1. Error Triggered: It only activates when the source Observable sends an error notification.
  2. Resubscription: It attempts the exact same operation again by resubscribing.
  3. Count: You can (and usually should) provide a number to retry(n) specifying how many times to retry after the initial failure. So, retry(2) means 1 initial attempt + 2 retries = 3 total attempts maximum.
  4. Success: If any attempt (initial or retry) is successful, the success value is passed through, and retry does nothing further for that subscription.
  5. Final Error: If all attempts (initial + all retries) fail, the error from the last attempt is sent down the Observable chain.
  6. Infinite Retries: Using retry() without a count will retry indefinitely upon error. This is usually dangerous!

Real-World Example#

Fetching data from a web server is a classic case. Sometimes, network requests fail due to temporary glitches, a brief server hiccup, or packet loss. You don't want your application to break immediately. Instead, you might want to automatically try fetching the data again once or twice before showing an error message to the user.

  • Source Observable: The HttpClient GET request Observable that fetches user data.
  • Potential Error: A network error or a 5xx server error (indicating a temporary server issue).

retry is ideal here to handle these transient failures gracefully.

Angular Code Snippet#

Let's create a service that fetches user data and a component that uses it, incorporating retry.

1. User Data Service (user.service.ts)

import { Injectable, inject } from "@angular/core";
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Observable, throwError, timer } from "rxjs";
import { retry, catchError, tap } from "rxjs/operators";

export interface UserData {
  id: number;
  name: string;
  email: string;
}

@Injectable({
  providedIn: "root",
})
export class UserService {
  private http = inject(HttpClient);
  private apiUrl = "/api/user/profile"; // Example API endpoint

  getUserProfile(): Observable<UserData> {
    console.log("Attempting to fetch user profile...");
    return this.http.get<UserData>(this.apiUrl).pipe(
      tap(() =>
        console.log("Fetched user profile successfully on an attempt.")
      ),
      // Retry configuration:
      // count: 2 - Retry up to 2 times after the initial failure (3 total attempts).
      // delay: 1000 - Wait 1 second between retries.
      retry({
        count: 2,
        delay: (error: HttpErrorResponse, retryCount: number) => {
          // Optional: Add more sophisticated delay logic (e.g., exponential backoff)
          // Only retry on specific error types (e.g., network or 5xx server errors)
          if (error.status >= 500 || error.status === 0) {
            // Server error or network error
            console.warn(
              `Attempt ${retryCount}: Retrying after error: ${error.message}. Waiting 1 second...`
            );
            return timer(1000); // Use RxJS timer for delay
          } else {
            // Don't retry for client errors (4xx) or other unexpected errors
            console.error(
              `Attempt ${retryCount}: Not retrying for error: ${error.message}`
            );
            return throwError(() => error); // Propagate the error immediately
          }
        },
      }),
      catchError((error: HttpErrorResponse) => {
        // This catchError runs AFTER retries have been exhausted OR if retry decides not to retry
        console.error("Failed to fetch user profile after all retries:", error);
        // You might want to return a user-friendly error object or re-throw
        return throwError(
          () =>
            new Error("Could not load user profile. Please try again later.")
        );
      })
    );
  }
}

2. User Profile Component (user-profile.component.ts)

import {
  Component,
  inject,
  signal,
  ChangeDetectionStrategy,
  OnInit,
  DestroyRef,
} from "@angular/core";
import { CommonModule } from "@angular/common";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { UserService, UserData } from "./user.service";
import { catchError, tap, finalize } from "rxjs/operators";
import { EMPTY } from "rxjs"; // Import EMPTY

@Component({
  selector: "app-user-profile",
  standalone: true,
  imports: [CommonModule],
  template: `
    <h3>User Profile</h3>
    @if (loading()) {
    <p>Loading user profile...</p>
    } @else if (errorMsg()) {
    <p style="color: red;">Error: {{ errorMsg() }}</p>
    } @else if (userProfile()) {
    <div>
      <p><strong>ID:</strong> {{ userProfile()?.id }}</p>
      <p><strong>Name:</strong> {{ userProfile()?.name }}</p>
      <p><strong>Email:</strong> {{ userProfile()?.email }}</p>
    </div>
    } @else {
    <p>Click the button to load profile.</p>
    }

    <button (click)="loadProfile()" [disabled]="loading()">Load Profile</button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserProfileComponent {
  private userService = inject(UserService);
  private destroyRef = inject(DestroyRef);

  // --- State Signals ---
  userProfile = signal<UserData | null>(null);
  loading = signal<boolean>(false);
  errorMsg = signal<string | null>(null);

  loadProfile(): void {
    this.loading.set(true);
    this.errorMsg.set(null); // Clear previous errors
    this.userProfile.set(null); // Clear previous data

    this.userService
      .getUserProfile()
      .pipe(
        tap((data) => console.log("Component received user data:", data)),
        // Automatically unsubscribe when the component is destroyed
        takeUntilDestroyed(this.destroyRef),
        // Handle final success or error within the component
        catchError((err: Error) => {
          console.error("Component caught final error:", err);
          this.errorMsg.set(err.message || "An unknown error occurred.");
          // Return EMPTY to gracefully complete the observable chain here
          // so the finalize operator still runs, but no further 'next' is expected.
          return EMPTY;
        }),
        finalize(() => {
          // This runs whether the observable completes successfully or errors out (after retries/catchError)
          this.loading.set(false);
          console.log("Finished loading attempt (success or final error).");
        })
      )
      .subscribe({
        next: (data) => {
          this.userProfile.set(data); // Update signal on success
        },
        // Error handling is done in catchError now, but could also be done here
        // error: (err) => { /* Already handled by catchError */ }
        // complete: () => { /* Optional: actions on completion */ }
      });
  }
}

Explanation:

  1. UserService:
    • getUserProfile makes an HTTP GET request.
    • .pipe() is used to chain operators.
    • retry({ count: 2, delay: ... }): If the http.get call results in an error, this configuration checks the error type. If it's a server-side error (>=500) or a network error (status === 0), it waits for 1 second (timer(1000)) and then tells RxJS to resubscribe (retry the http.get). This happens up to 2 times after the first failure. If it's a client error (like 404 Not Found), it uses throwError(() => error) to stop retrying immediately.
    • catchError: This operator runs only if the retry logic gives up (either exhausts retries or decides not to retry based on the error type). It catches the final error, logs it, and returns a new error observable (throwError) with a user-friendly message.
  2. UserProfileComponent:
    • It uses Signals (userProfile, loading, errorMsg) to manage the component's state reactively.
    • loadProfile triggers the process.
    • It calls userService.getUserProfile().
    • takeUntilDestroyed(this.destroyRef) ensures the subscription is cleaned up.
    • catchError: Catches the final error passed down from the service (after retries) and updates the errorMsg signal. Returning EMPTY prevents the error from propagating further and allows finalize to run.
    • finalize: This operator runs regardless of whether the stream completed successfully or errored out (after retries/catchError). It's perfect for setting loading.set(false).
    • .subscribe(): Updates the userProfile signal when data is successfully received.

Important Considerations for retry#

  • Don't Retry Everything: Only retry operations that might succeed on a subsequent attempt (transient network/server errors). Don't retry things like "404 Not Found" or "400 Bad Request" errors, as retrying won't fix the underlying problem. The delay function in the retry configuration object is ideal for this conditional retrying logic.
  • Avoid Infinite Retries: Always specify a reasonable retry count.
  • Side Effects: Be very careful retrying Observables that have side effects (e.g., POST or PUT requests). Retrying might cause the action (like creating a resource) to happen multiple times. It's generally safer for idempotent requests like GET or DELETE.
  • Delay: Retrying immediately might not be helpful if the server needs a moment to recover. Adding a delay, possibly increasing with each retry (exponential backoff), is often recommended. The retry({ delay: ... }) configuration makes this much easier than the older retryWhen operator.