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#
- Error Triggered: It only activates when the source Observable sends an error notification.
- Resubscription: It attempts the exact same operation again by resubscribing.
- 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. - Success: If any attempt (initial or retry) is successful, the success value is passed through, and
retry
does nothing further for that subscription. - Final Error: If all attempts (initial + all retries) fail, the error from the last attempt is sent down the Observable chain.
- 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:
UserService
:getUserProfile
makes an HTTP GET request..pipe()
is used to chain operators.retry({ count: 2, delay: ... })
: If thehttp.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 thehttp.get
). This happens up to 2 times after the first failure. If it's a client error (like 404 Not Found), it usesthrowError(() => error)
to stop retrying immediately.catchError
: This operator runs only if theretry
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.
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 theerrorMsg
signal. ReturningEMPTY
prevents the error from propagating further and allowsfinalize
to run.finalize
: This operator runs regardless of whether the stream completed successfully or errored out (after retries/catchError
). It's perfect for settingloading.set(false)
..subscribe()
: Updates theuserProfile
signal when data is successfully received.
- It uses Signals (
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 theretry
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 olderretryWhen
operator.