Skip to main content

Angular Error Handling

When developing Angular applications that communicate with backend services through HTTP requests, things don't always go as planned. Network issues, server errors, or invalid requests can cause your HTTP calls to fail. Without proper error handling, these failures can lead to broken user experiences and difficult-to-debug problems.

In this guide, we'll explore how to implement robust error handling strategies for HTTP requests in Angular applications.

Understanding HTTP Errors in Angular

Before diving into implementation, it's important to understand the types of errors you might encounter:

  1. Client-side errors (4xx status codes): These occur when the client makes an improper request, such as requesting a non-existent resource (404) or unauthorized access (401)
  2. Server-side errors (5xx status codes): These happen when the server fails to fulfill a valid request
  3. Network errors: These occur when there are connectivity issues preventing the request from reaching the server

Angular's HttpClient returns Observables that don't automatically execute until you subscribe to them. When an HTTP error occurs, the Observable doesn't emit a value but instead emits an error notification.

Basic Error Handling with catchError

The most common way to handle errors in Angular HTTP requests is using RxJS operators, particularly catchError.

First, let's import the necessary modules:

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

Now, let's create a service that handles errors:

typescript
@Injectable({
providedIn: 'root'
})
export class DataService {
private apiUrl = 'https://api.example.com/items';

constructor(private http: HttpClient) { }

getItems(): Observable<any[]> {
return this.http.get<any[]>(this.apiUrl)
.pipe(
catchError(this.handleError)
);
}

private handleError(error: HttpErrorResponse) {
if (error.error instanceof ErrorEvent) {
// Client-side or network error occurred
console.error('An error occurred:', error.error.message);
} else {
// Backend returned an unsuccessful response code.
console.error(
`Backend returned code ${error.status}, ` +
`body was: ${error.error}`);
}
// Return an observable with a user-facing error message
return throwError('Something bad happened; please try again later.');
}
}

In this example, catchError intercepts any errors and passes them to our handleError method. The method identifies whether the error is client-side or server-side and returns a new Observable created with throwError that emits a user-friendly error message.

Using the Error Handler in Components

Now, let's see how a component would use this service:

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

@Component({
selector: 'app-items',
template: `
<div *ngIf="items">
<ul>
<li *ngFor="let item of items">{{ item.name }}</li>
</ul>
</div>
<div *ngIf="errorMessage" class="error">
{{ errorMessage }}
</div>
<div *ngIf="loading" class="loading">
Loading...
</div>
`
})
export class ItemsComponent implements OnInit {
items: any[];
errorMessage: string;
loading = true;

constructor(private dataService: DataService) { }

ngOnInit() {
this.dataService.getItems()
.subscribe(
(data) => {
this.items = data;
this.loading = false;
},
(error) => {
this.errorMessage = error;
this.loading = false;
}
);
}
}

This component subscribes to the getItems() method and handles both successful responses and errors.

Advanced Error Handling with retry

Sometimes temporary network issues can cause requests to fail. In these cases, it might be helpful to retry the request before giving up:

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

@Injectable({
providedIn: 'root'
})
export class DataService {
private apiUrl = 'https://api.example.com/items';

constructor(private http: HttpClient) { }

getItems(): Observable<any[]> {
return this.http.get<any[]>(this.apiUrl)
.pipe(
retry(2), // Retry failed request up to 2 times
catchError(this.handleError)
);
}

// handleError method remains the same
}

In this example, the retry operator will attempt to redo the HTTP request up to 2 times before finally giving up and letting the error pass through to catchError.

Using retryWhen for More Control

For more advanced retrying logic, you can use retryWhen:

typescript
import { retryWhen, delay, take, concat, throwError } from 'rxjs/operators';

getItems(): Observable<any[]> {
return this.http.get<any[]>(this.apiUrl)
.pipe(
retryWhen(errors =>
errors.pipe(
// Add a delay of 1 second
delay(1000),
// Only retry 3 times
take(3),
// Then throw the error
concat(throwError('Maximum retries exceeded'))
)
),
catchError(this.handleError)
);
}

This will retry the request up to 3 times with a 1-second delay between attempts.

Global Error Handling

For application-wide error handling, Angular provides the ErrorHandler class which can be extended:

typescript
import { ErrorHandler, Injectable } from '@angular/core';

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
handleError(error: any): void {
// Log to console
console.error('Global error handler caught an error:', error);

// You could also send this error to a logging service
// this.loggingService.logError(error);
}
}

Then, provide this in your app module:

typescript
import { NgModule, ErrorHandler } from '@angular/core';
import { GlobalErrorHandler } from './global-error-handler';

@NgModule({
// other module properties
providers: [
{ provide: ErrorHandler, useClass: GlobalErrorHandler }
]
})
export class AppModule { }

This approach is useful for catching uncaught exceptions in your application, not just HTTP errors.

HTTP Interceptors for Error Handling

HTTP interceptors provide a powerful way to handle errors for all HTTP requests in your application:

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

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request)
.pipe(
catchError((error: HttpErrorResponse) => {
let errorMessage = '';

if (error.error instanceof ErrorEvent) {
// Client-side error
errorMessage = `Error: ${error.error.message}`;
} else {
// Server-side error
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;

// Handle specific status codes
switch (error.status) {
case 401:
// Unauthorized - redirect to login
console.log('You need to log in.');
// this.router.navigate(['/login']);
break;
case 404:
// Resource not found
console.log('The requested resource was not found.');
break;
}
}

// Show an error message to the user
console.error(errorMessage);

return throwError(errorMessage);
})
);
}
}

Register the interceptor in your app module:

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

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

Real-World Example: Loading State and Error Messages

Here's a more complete example showing how to manage loading states and error messages in a real-world application:

typescript
// user.service.ts
@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = 'https://api.example.com/users';

constructor(private http: HttpClient) { }

getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl)
.pipe(
catchError(this.handleError<User[]>('getUsers', []))
);
}

private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
console.error(`${operation} failed: ${error.message}`);

// Let the app keep running by returning an empty result
return of(result as T);
};
}
}

// user-list.component.ts
@Component({
selector: 'app-user-list',
template: `
<div class="users-container">
<h2>Users</h2>

<div *ngIf="loading" class="loading-spinner">
<mat-spinner></mat-spinner>
</div>

<div *ngIf="errorMessage" class="error-container">
<div class="alert alert-danger">
{{ errorMessage }}
<button (click)="retry()" class="btn btn-primary">Retry</button>
</div>
</div>

<ul *ngIf="!loading && !errorMessage" class="user-list">
<li *ngFor="let user of users" class="user-item">
{{ user.name }} ({{ user.email }})
</li>
</ul>
</div>
`
})
export class UserListComponent implements OnInit {
users: User[] = [];
loading = false;
errorMessage = '';

constructor(private userService: UserService) { }

ngOnInit() {
this.loadUsers();
}

loadUsers() {
this.loading = true;
this.errorMessage = '';

this.userService.getUsers()
.pipe(
finalize(() => this.loading = false)
)
.subscribe(
(users) => this.users = users,
(error) => this.errorMessage = 'Failed to load users. Please try again later.'
);
}

retry() {
this.loadUsers();
}
}

In this example, we:

  1. Use a loading indicator to show when the request is in progress
  2. Display an error message when the request fails
  3. Provide a retry button to let users attempt the operation again
  4. Use the finalize operator to ensure the loading state is set to false regardless of whether the request succeeds or fails

Summary

Effective error handling is critical for building robust Angular applications. In this guide, we've covered:

  1. Basic error handling with catchError
  2. Retrying failed requests with retry and retryWhen
  3. Implementing global error handlers
  4. Using HTTP interceptors for centralized error handling
  5. Creating user-friendly interfaces that handle loading states and errors

Remember these key principles for good error handling:

  • Always catch and handle errors from HTTP requests
  • Provide meaningful feedback to users when errors occur
  • Log errors for debugging purposes
  • Consider retrying failed requests when appropriate
  • Implement a consistent error handling strategy across your application

Additional Resources

Exercises

  1. Modify the DataService to handle specific HTTP status codes differently (e.g., 401, 403, 404)
  2. Create a reusable error component that displays different messages based on error types
  3. Implement an HTTP interceptor that adds authentication headers and handles 401 errors by redirecting to a login page
  4. Build a service that retries failed requests with exponential backoff (increasing delay between retries)
  5. Extend the global error handler to send errors to a logging service

By following these practices, you'll build more resilient Angular applications that gracefully handle errors and provide a better user experience.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)