Error Handling with catchError in Angular HttpClient

Error handling is one of the most critical aspects of building reliable, user-friendly web applications. In Angular, the HttpClient service enables us to communicate with backend APIs using observables. However, since network requests can fail for many reasons—such as invalid URLs, authentication errors, or server downtime—handling these errors properly is essential to ensure a smooth user experience.

Angular leverages RxJS operators, and among them, catchError is the primary tool for managing and responding to API errors gracefully.

This post will explore how catchError works, how to integrate it in real-world Angular applications, how to combine it with throwError, and how to create global error handling strategies that prevent app crashes and provide helpful feedback to users.

1. Understanding the Need for Error Handling in Angular

HTTP requests are inherently unreliable. A request to a remote server may fail due to network issues, incorrect endpoints, server-side validation errors, or expired authentication tokens.

Without proper error handling, such failures can lead to broken user interfaces, confusing behavior, or even crashes. Angular’s HttpClient combined with RxJS offers a robust way to handle these scenarios using reactive programming patterns.

Common Causes of HTTP Errors

  1. Incorrect API endpoint URLs
  2. Server downtime or maintenance
  3. Authentication or authorization failures (401, 403)
  4. Invalid request payloads (400 Bad Request)
  5. Server errors (500 Internal Server Error)
  6. Network disconnections or timeouts

Angular provides a mechanism to intercept these issues before they cause problems in the user interface — and that’s where catchError comes in.


2. Introducing catchError Operator

The catchError operator is part of the RxJS library and allows you to handle errors that occur during observable execution.

When used with the Angular HttpClient, catchError intercepts any HTTP error and gives you an opportunity to respond — such as showing an error message, retrying the request, or logging the issue.


Basic Syntax of catchError

import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';

this.http.get('/api/data').pipe(
  catchError(error => {
console.error('Error occurred:', error);
return throwError(() => error);
}) );

In this code:

  • The request is wrapped in an observable stream.
  • If an error occurs, catchError catches it.
  • Inside the callback, you can log the error or transform it.
  • throwError re-emits the error, allowing it to be handled elsewhere in your application.

3. Setting Up a Simple Angular Example

Before diving deep, let’s set up a minimal Angular project structure to demonstrate error handling with HttpClient.

Step 1: Import HttpClientModule

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule],
  bootstrap: [AppComponent]
})
export class AppModule {}

This enables HttpClient throughout your Angular app.


Step 2: Create a Service for API Calls

ng generate service services/data

This creates data.service.ts in your src/app/services folder.


Step 3: Implement Basic HTTP Call with catchError

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

@Injectable({
  providedIn: 'root'
})
export class DataService {
  private apiUrl = 'https://jsonplaceholder.typicode.com/posts';

  constructor(private http: HttpClient) {}

  getData(): Observable<any> {
return this.http.get(this.apiUrl).pipe(
  catchError(this.handleError)
);
} private handleError(error: HttpErrorResponse) {
console.error('Error status:', error.status);
console.error('Error body:', error.error);
if (error.error instanceof ErrorEvent) {
  console.error('Client-side error:', error.error.message);
} else {
  console.error(Server-side error: ${error.status} - ${error.message});
}
return throwError(() =&gt; new Error('Something went wrong, please try again.'));
} }

Explanation

  1. HttpErrorResponse: Provides detailed info about the HTTP error.
  2. catchError: Intercepts the observable stream when an error occurs.
  3. throwError: Returns a new observable that emits an error.
  4. handleError(): A centralized method for logging and formatting errors.

This structure allows your Angular application to handle unexpected API failures smoothly.


4. Handling Different Error Types

Errors can come from either the client or the server, and distinguishing between them is crucial.

Client-Side or Network Errors

These errors occur when something goes wrong in the browser or network — for example, when the user’s internet connection drops.

if (error.error instanceof ErrorEvent) {
  console.error('A client-side or network error occurred:', error.error.message);
}

Server-Side Errors

These errors occur when the backend API responds with an HTTP status code that indicates failure (400, 404, 500, etc.).

else {
  console.error(Backend returned code ${error.status}, body was: ${error.error});
}

This pattern ensures that your Angular app reacts differently based on where the error originates.


5. Using catchError in a Component

You can handle errors either in the service (as shown earlier) or directly in the component where the service is consumed.

Example Component

import { Component, OnInit } from '@angular/core';
import { DataService } from '../services/data.service';

@Component({
  selector: 'app-posts',
  template: `
&lt;h2&gt;Post List&lt;/h2&gt;
&lt;div *ngIf="errorMessage" class="error"&gt;{{ errorMessage }}&lt;/div&gt;
&lt;ul&gt;
  &lt;li *ngFor="let post of posts"&gt;{{ post.title }}&lt;/li&gt;
&lt;/ul&gt;
` }) export class PostsComponent implements OnInit { posts: any[] = []; errorMessage: string = ''; constructor(private dataService: DataService) {} ngOnInit(): void {
this.dataService.getData().subscribe({
  next: (data) =&gt; (this.posts = data),
  error: (err) =&gt; (this.errorMessage = err.message)
});
} }

In this example:

  • If an error occurs, catchError in the service triggers.
  • The error is re-thrown using throwError.
  • The component subscribes to the error and displays it on the page.

6. Handling Errors Globally with Interceptors

For large applications, handling errors in each service separately can become repetitive.
A better approach is to create a global HTTP interceptor that catches errors across all requests.

Example: Error Interceptor

import { Injectable } from '@angular/core';
import {
  HttpInterceptor,
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
  catchError((error: HttpErrorResponse) =&gt; {
    if (error.status === 401) {
      console.error('Unauthorized access.');
    } else if (error.status === 404) {
      console.error('Resource not found.');
    } else if (error.status === 500) {
      console.error('Server error occurred.');
    } else {
      console.error('An unknown error occurred:', error);
    }
    return throwError(() =&gt; new Error('Something went wrong. Please try again later.'));
  })
);
} }

Registering the Interceptor

import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { ErrorInterceptor } from './error.interceptor';

@NgModule({
  providers: [
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }
] }) export class AppModule {}

Now every HTTP request in the app automatically passes through this interceptor.


7. Displaying User-Friendly Error Messages

Instead of showing raw technical messages, it’s better to display simple, readable notifications to users.

Example: Using a Notification Service

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class NotificationService {
  showError(message: string) {
alert(message);
} }

Use this service inside your interceptor or components to display alerts when an error occurs.

import { NotificationService } from './notification.service';

constructor(private notification: NotificationService) {}

catchError(error => {
  this.notification.showError('Failed to fetch data. Please try again.');
  return throwError(() => error);
});

8. Retrying Failed Requests

Sometimes network errors are temporary. You can retry failed requests automatically using the retry() operator from RxJS.

import { retry, catchError } from 'rxjs/operators';

this.http.get('/api/data').pipe(
  retry(3),
  catchError(error => {
console.error('Error after retries:', error);
return throwError(() =&gt; error);
}) );

This code retries the failed HTTP call up to three times before giving up and passing the error downstream.


9. Combining catchError with Other Operators

You can combine catchError with operators like map, tap, or switchMap to process responses and handle errors elegantly.

Example

import { map, catchError } from 'rxjs/operators';

this.http.get('/api/data').pipe(
  map(response => response as any[]),
  catchError(error => {
console.error('Mapping failed:', error);
return throwError(() =&gt; error);
}) );

This ensures that you can transform data while still keeping error handling consistent.


10. Using Custom Error Models

For more structured applications, it’s useful to define custom error objects.

Example Custom Error Interface

export interface ApiError {
  message: string;
  statusCode: number;
  timestamp: string;
}

Example of Returning Structured Error

catchError((error: HttpErrorResponse) => {
  const apiError: ApiError = {
message: error.message,
statusCode: error.status,
timestamp: new Date().toISOString()
}; return throwError(() => apiError); });

This way, the rest of your app can rely on consistent error shapes.


11. Testing Error Handling with Mock APIs

During development, you can use mock APIs to test different failure cases.

Example: Simulating Error with Wrong URL

this.http.get('https://jsonplaceholder.typicode.com/invalid-endpoint').pipe(
  catchError(error => {
console.error('Simulated error:', error);
return throwError(() =&gt; error);
}) );

This request will produce a 404 Not Found error, allowing you to test how your UI behaves.


12. Advanced Example – Handling Different Status Codes

You can handle specific status codes differently inside catchError.

catchError((error: HttpErrorResponse) => {
  switch (error.status) {
case 400:
  console.error('Bad Request');
  break;
case 401:
  console.error('Unauthorized');
  break;
case 403:
  console.error('Forbidden');
  break;
case 404:
  console.error('Not Found');
  break;
case 500:
  console.error('Server Error');
  break;
default:
  console.error('Unknown Error');
} return throwError(() => error); });

This approach provides fine-grained control over how each type of error is handled.


13. Avoiding Common Mistakes

1. Forgetting to Return throwError

If you forget to return an observable in catchError, the stream will break.

Incorrect

catchError(error => {
  console.error(error);
});

Correct

catchError(error => {
  console.error(error);
  return throwError(() => error);
});

2. Using catchError Without pipe

Always use catchError within an observable pipeline, not as a standalone function.

Incorrect

this.http.get('/api/data', catchError(...));

Correct

this.http.get('/api/data').pipe(catchError(...));

14. Reusable Error Handler Function

You can define a reusable function to handle all errors consistently.

function handleApiError(error: HttpErrorResponse) {
  console.error('Error:', error);
  return throwError(() => new Error('API request failed.'));
}

Use it across multiple services:

this.http.get('/api/users').pipe(catchError(handleApiError));
this.http.post('/api/posts', data).pipe(catchError(handleApiError));

This improves maintainability and reduces redundancy.


15. Logging Errors to a Server

You can also log API errors to a remote server for debugging or analytics purposes.

Example

import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class LoggingService {
  constructor(private http: HttpClient) {}

  logError(error: any) {
this.http.post('https://api.example.com/logs', { error }).subscribe();
} }

Use this inside catchError to store logs centrally.


16. Handling Non-HTTP Errors

Sometimes, errors may not come from the server but from other asynchronous operations. You can still use catchError to handle them.

of(JSON.parse('invalid JSON')).pipe(
  catchError(err => {
console.error('Parsing failed:', err);
return throwError(() =&gt; new Error('Invalid data.'));
}) );

This demonstrates that catchError works for all observable errors, not just HTTP.


17. Full Example: DataService with Advanced Error Handling

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

@Injectable({
  providedIn: 'root'
})
export class DataService {
  private apiUrl = 'https://jsonplaceholder.typicode.com/posts';

  constructor(private http: HttpClient) {}

  getPosts(): Observable<any[]> {
return this.http.get&lt;any&#91;]&gt;(this.apiUrl).pipe(
  retry(2),
  catchError(this.handleError)
);
} private handleError(error: HttpErrorResponse) {
console.error('HTTP error:', error.status, error.message);
if (error.error instanceof ErrorEvent) {
  console.error('Client/network error:', error.error.message);
} else {
  console.error(Backend returned code ${error.status});
}
return throwError(() =&gt; new Error('Failed to load posts. Please try again.'));
} }

18. Summary of Best Practices

  1. Always use catchError with throwError to propagate errors safely.
  2. Handle both client-side and server-side errors.
  3. Create reusable error-handling functions or services.
  4. Use interceptors for global error handling.
  5. Log errors for monitoring and analytics.
  6. Display user-friendly messages instead of technical details.
  7. Use retry() for transient network errors.
  8. Avoid duplicating error logic across services.
  9. Return typed errors when possible for consistency.
  10. Test all major failure scenarios during development.

19. Complete End-to-End Example

// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { AppComponent } from './app.component';
import { ErrorInterceptor } from './error.interceptor';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule],
  providers: [
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }
], bootstrap: [AppComponent] }) export class AppModule {}
// error.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpInterceptor, HttpRequest, HttpHandler, HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> {
return next.handle(req).pipe(
  catchError((error: HttpErrorResponse) =&gt; {
    console.error('Global error caught:', error.message);
    return throwError(() =&gt; new Error('Request failed.'));
  })
);
} }
// data.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class DataService {
  constructor(private http: HttpClient) {}

  getData(): Observable<any> {
return this.http.get('/api/data').pipe(
  catchError(err =&gt; {
    console.error('Service-level error:', err);
    throw err;
  })
);
} }

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *