catchError
catchError()
is an RxJS operator used for graceful error handling within an Observable stream. When an error occurs in the source Observable (or in any preceding operators in the pipe
), catchError
intercepts that error notification. It gives you a chance to:
- Analyze the error: Log it, send it to a monitoring service, etc.
- Attempt recovery: You might retry the operation (often using the
retry
operator beforecatchError
). - Provide a fallback value: Return a default value or an empty state so the stream can continue gracefully instead of just terminating.
- Re-throw the error: If you can't handle it, you can let the error propagate further down the chain to the subscriber's error handler.
How catchError
Works#
- You place
catchError
inside the.pipe()
method. - It takes a function as an argument. This function receives the
error
object and optionally thecaught
Observable (the source stream that errored, useful for retrying). - Crucially, this function MUST return a new Observable.
- If you return
of(someDefaultValue)
(e.g.,of([])
,of(null)
), the outer stream will receive that default value in itsnext
handler, and then it will complete successfully (it won't hit theerror
handler of the subscription). - If you return
EMPTY
(from RxJS), the outer stream simply completes without emitting any further values. - If you
throw error
(orthrow new Error(...)
) inside thecatchError
function, the error is passed along to theerror
handler of yoursubscribe
block (or the nextcatchError
downstream).
Real-World Example: Handling HTTP Request Errors#
This is the most common use case in Angular. Imagine fetching user data from an API. The API might fail for various reasons (server down, user not found, network issue). Instead of letting the error break your component, you want to catch it, show a message, and perhaps return null
or an empty user object.
Code Snippet Example#
Let's create a component that tries to fetch user data. We'll use catchError
to handle potential HttpClient
errors.
import { Component, DestroyRef, inject, OnInit, signal } from "@angular/core";
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Observable, of, EMPTY, throwError } from "rxjs"; // Import 'of', 'EMPTY', 'throwError'
import { catchError, tap } from "rxjs/operators";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
interface User {
id: number;
name: string;
email: string;
}
@Component({
selector: "app-user-profile",
standalone: true,
imports: [], // Add CommonModule if using *ngIf/*ngFor later
template: `
<h2>User Profile</h2>
<div *ngIf="loading()">Loading user data...</div>
<div *ngIf="user() as userData">
<p>ID: {{ userData.id }}</p>
<p>Name: {{ userData.name }}</p>
<p>Email: {{ userData.email }}</p>
</div>
<div *ngIf="errorMsg()" style="color: red;">Error: {{ errorMsg() }}</div>
<button (click)="loadUser()" [disabled]="loading()">Reload User</button>
`,
})
export class UserProfileComponent implements OnInit {
private http = inject(HttpClient);
private destroyRef = inject(DestroyRef);
// --- State Signals ---
user = signal<User | null>(null);
loading = signal<boolean>(false);
errorMsg = signal<string | null>(null);
private userId = 1; // Example user ID
ngOnInit() {
this.loadUser();
}
loadUser() {
this.loading.set(true);
this.errorMsg.set(null); // Clear previous errors
this.user.set(null); // Clear previous user data
this.fetchUserData(this.userId)
.pipe(
takeUntilDestroyed(this.destroyRef) // Auto-unsubscribe on destroy
)
.subscribe({
next: (userData) => {
this.user.set(userData); // Update user signal on success
this.loading.set(false);
console.log("User data loaded:", userData);
},
error: (err) => {
// This error handler is called ONLY if catchError re-throws the error
this.loading.set(false);
// Error is already set by catchError if we return a fallback
// If catchError re-threw, we might set a generic message here
if (!this.errorMsg()) {
// Check if message wasn't set by catchError
this.errorMsg.set("An unexpected error occurred downstream.");
}
console.error("Subscription Error Handler:", err);
},
complete: () => {
// Called when the stream finishes successfully
// (including after catchError returns a fallback like 'of(null)')
this.loading.set(false); // Ensure loading is off
console.log("User data stream completed.");
},
});
}
private fetchUserData(id: number): Observable<User | null> {
// Use a non-existent URL to force an error for demonstration
// const apiUrl = `/api/users/${id}`; // Real URL
const apiUrl = `/api/non-existent-users/${id}`; // Fake URL for testing error
return this.http.get<User>(apiUrl).pipe(
tap(() => console.log(`Attempting to fetch user ${id}`)), // Side effect logging
// --- catchError Operator ---
catchError((error: HttpErrorResponse) => {
console.error("HTTP Error intercepted by catchError:", error);
// ---- Strategy 1: Handle and return a fallback value ----
// Set user-friendly error message
if (error.status === 404) {
this.errorMsg.set(`User with ID ${id} not found.`);
} else if (error.status === 0 || error.status >= 500) {
this.errorMsg.set(
"Server error or network issue. Please try again later."
);
} else {
this.errorMsg.set(`An error occurred: ${error.message}`);
}
this.loading.set(false); // Turn off loading indicator
// Return null as a fallback. The 'next' handler of subscribe will receive null.
return of(null);
// ---- Strategy 2: Handle and return EMPTY (completes without value) ----
// this.errorMsg.set('Could not load user data.');
// this.loading.set(false);
// return EMPTY; // Stream completes, 'next' handler is not called.
// ---- Strategy 3: Log and re-throw the error ----
// this.errorMsg.set('Failed to load user data. Error propagated.'); // Set msg here or in subscribe error block
// this.loading.set(false);
// return throwError(() => new Error(`Failed fetching user ${id}: ${error.message}`)); // Propagate error to subscribe's error handler
})
);
}
}
Explanation:
WorkspaceUserData
makes an HTTP GET request usingHttpClient
.- We
.pipe()
the result throughcatchError
. - If the HTTP request fails (e.g., returns 404 Not Found), the
catchError
function executes. - Inside
catchError
:- We log the actual
HttpErrorResponse
. - We set a user-friendly error message in the
errorMsg
signal based on the error status. - We set
loading
to false. - Crucially, we return
of(null)
. This creates a new observable that emitsnull
once and then completes.
- We log the actual
- Because
catchError
returnedof(null)
, the original stream is considered "handled." Thesubscribe
block'snext
handler receivesnull
. The component updates theuser
signal tonull
and theerror
handler is not executed. The stream completes normally. - The template uses
*ngIf
directives bound to the signals (user()
,errorMsg()
,loading()
) to conditionally display the user data, the loading indicator, or the error message.
If we had chosen Strategy 3 (using throwError
), the error would propagate to the subscribe
block's error
handler.
catchError
is essential for building robust Angular applications that can gracefully handle failures in asynchronous operations like API calls.