Skip to content

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:

  1. Analyze the error: Log it, send it to a monitoring service, etc.
  2. Attempt recovery: You might retry the operation (often using the retry operator before catchError).
  3. Provide a fallback value: Return a default value or an empty state so the stream can continue gracefully instead of just terminating.
  4. 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 the caught 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 its next handler, and then it will complete successfully (it won't hit the error handler of the subscription).
  • If you return EMPTY (from RxJS), the outer stream simply completes without emitting any further values.
  • If you throw error (or throw new Error(...)) inside the catchError function, the error is passed along to the error handler of your subscribe block (or the next catchError 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:

  1. WorkspaceUserData makes an HTTP GET request using HttpClient.
  2. We .pipe() the result through catchError.
  3. If the HTTP request fails (e.g., returns 404 Not Found), the catchError function executes.
  4. 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 emits null once and then completes.
  5. Because catchError returned of(null), the original stream is considered "handled." The subscribe block's next handler receives null. The component updates the user signal to null and the error handler is not executed. The stream completes normally.
  6. 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.