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:
- 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)
- Server-side errors (5xx status codes): These happen when the server fails to fulfill a valid request
- 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:
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:
@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:
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:
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
:
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:
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:
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:
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:
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:
// 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:
- Use a loading indicator to show when the request is in progress
- Display an error message when the request fails
- Provide a retry button to let users attempt the operation again
- 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:
- Basic error handling with
catchError
- Retrying failed requests with
retry
andretryWhen
- Implementing global error handlers
- Using HTTP interceptors for centralized error handling
- 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
- Modify the
DataService
to handle specific HTTP status codes differently (e.g., 401, 403, 404) - Create a reusable error component that displays different messages based on error types
- Implement an HTTP interceptor that adds authentication headers and handles 401 errors by redirecting to a login page
- Build a service that retries failed requests with exponential backoff (increasing delay between retries)
- 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! :)