Angular Error Handling
Error handling is a critical aspect of building robust Angular applications. Properly implemented error handling not only helps developers identify and fix issues but also ensures a smooth user experience even when things go wrong. In this guide, we'll explore various techniques and best practices for handling errors in Angular applications.
Introduction to Error Handling in Angular
Modern applications can encounter various types of errors:
- Network failures when communicating with servers
- Invalid user inputs that need validation
- Runtime exceptions in your application code
- Server-side errors returned by APIs
Without proper error handling, these issues can lead to:
- Crashed applications
- Blank screens or frozen UIs
- Confused users without helpful feedback
- Security vulnerabilities
- Difficult-to-debug production issues
Let's dive into how Angular helps us manage these scenarios effectively.
Global Error Handling
Using ErrorHandler
Angular provides a built-in ErrorHandler
class that you can extend to implement global error handling for your application.
First, create a custom error handler:
// app/services/global-error-handler.service.ts
import { ErrorHandler, Injectable, NgZone } from '@angular/core';
import { NotificationService } from './notification.service';
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
constructor(
private notificationService: NotificationService,
private zone: NgZone
) {}
handleError(error: any): void {
// Make sure UI updates are properly triggered with NgZone
this.zone.run(() => {
console.error('Error caught by global error handler:', error);
// Display user-friendly message
this.notificationService.showError('An unexpected error occurred. Our team has been notified.');
// You could also log to an error monitoring service like Sentry
// this.loggingService.logError(error);
});
}
}
Then register it in your app module:
// app/app.module.ts
import { ErrorHandler, NgModule } from '@angular/core';
import { GlobalErrorHandler } from './services/global-error-handler.service';
@NgModule({
// other module configuration...
providers: [
{ provide: ErrorHandler, useClass: GlobalErrorHandler }
]
})
export class AppModule { }
HTTP Error Interceptors
For HTTP errors specifically, Angular provides HTTP interceptors that can centralize error handling for all API calls.
// app/interceptors/http-error.interceptor.ts
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';
import { NotificationService } from '../services/notification.service';
@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
constructor(private notificationService: NotificationService) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
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
switch (error.status) {
case 404:
errorMessage = 'Resource not found';
break;
case 403:
errorMessage = 'You do not have permission to access this resource';
break;
case 500:
errorMessage = 'Server error. Please try again later.';
break;
default:
errorMessage = `Error Code: ${error.status}, Message: ${error.message}`;
}
}
this.notificationService.showError(errorMessage);
return throwError(() => new Error(errorMessage));
})
);
}
}
Register the interceptor in your app module:
// app/app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { HttpErrorInterceptor } from './interceptors/http-error.interceptor';
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: HttpErrorInterceptor,
multi: true
}
]
})
export class AppModule { }
Component-Level Error Handling
Try-Catch Blocks
For synchronous code, you can use traditional try-catch blocks:
// In a component
submitForm(): void {
try {
const result = this.processFormData(this.form.value);
this.router.navigate(['/success']);
} catch (error) {
this.errorMessage = 'Failed to process form. Please check your inputs.';
console.error('Form processing error:', error);
}
}
RxJS Error Handling
For asynchronous operations, especially when working with Observables, RxJS provides powerful error handling operators:
catchError
import { Component, OnInit } from '@angular/core';
import { UserService } from '../services/user.service';
import { catchError } from 'rxjs/operators';
import { of } from 'rxjs';
@Component({
selector: 'app-user-profile',
template: `
<div *ngIf="loading">Loading profile...</div>
<div *ngIf="error" class="error-message">
{{ error }}
</div>
<div *ngIf="user">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
`
})
export class UserProfileComponent implements OnInit {
user: any;
loading = false;
error: string | null = null;
constructor(private userService: UserService) {}
ngOnInit(): void {
this.loading = true;
this.userService.getCurrentUser()
.pipe(
catchError(error => {
this.error = 'Failed to load user profile. Please try again later.';
console.error('User profile error:', error);
return of(null); // Return a safe value
})
)
.subscribe(user => {
this.user = user;
this.loading = false;
});
}
}
retry
You can also attempt to recover from transient errors by retrying operations:
import { retry, catchError } from 'rxjs/operators';
this.dataService.fetchData()
.pipe(
retry(3), // Retry up to 3 times before failing
catchError(error => {
this.errorMessage = 'Connection problem. Please try again.';
return of([]); // Return empty data
})
)
.subscribe(data => {
this.data = data;
});
finalize
Use finalize
to guarantee execution of cleanup code, similar to a "finally" block:
import { finalize } from 'rxjs/operators';
this.isLoading = true;
this.dataService.fetchData()
.pipe(
catchError(error => {
this.handleError(error);
return of(null);
}),
finalize(() => {
this.isLoading = false; // Always executed, even after errors
})
)
.subscribe(data => {
if (data) {
this.processData(data);
}
});
Practical Error Handling Strategies
Form Validation Errors
Angular provides built-in form validation that you can use to prevent errors before they happen:
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-registration',
template: `
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
<div>
<label for="email">Email</label>
<input id="email" type="email" formControlName="email">
<div *ngIf="email.invalid && (email.dirty || email.touched)" class="error">
<div *ngIf="email.errors?.['required']">Email is required.</div>
<div *ngIf="email.errors?.['email']">Please enter a valid email address.</div>
</div>
</div>
<div>
<label for="password">Password</label>
<input id="password" type="password" formControlName="password">
<div *ngIf="password.invalid && (password.dirty || password.touched)" class="error">
<div *ngIf="password.errors?.['required']">Password is required.</div>
<div *ngIf="password.errors?.['minlength']">
Password must be at least 8 characters long.
</div>
</div>
</div>
<button type="submit" [disabled]="registerForm.invalid">Register</button>
</form>
`,
styles: [`
.error { color: red; font-size: 12px; }
`]
})
export class RegistrationComponent {
registerForm: FormGroup;
constructor(private fb: FormBuilder) {
this.registerForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]]
});
}
get email() { return this.registerForm.get('email')!; }
get password() { return this.registerForm.get('password')!; }
onSubmit() {
if (this.registerForm.valid) {
// Process form data
}
}
}
Custom Error Pages
For certain errors (like 404 Not Found), you can configure routes to custom error pages:
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { NotFoundComponent } from './components/not-found/not-found.component';
const routes: Routes = [
// Your regular routes...
{ path: '**', component: NotFoundComponent } // Catch-all route for 404
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Error Boundaries
Angular doesn't have React-like error boundaries built-in, but you can create components that handle their own errors:
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-error-boundary',
template: `
<ng-container *ngIf="!hasError; else errorTemplate">
<ng-content></ng-content>
</ng-container>
<ng-template #errorTemplate>
<div class="error-boundary">
<h2>Something went wrong</h2>
<p>{{ errorMessage }}</p>
<button (click)="retry()">Try Again</button>
</div>
</ng-template>
`
})
export class ErrorBoundaryComponent implements OnInit {
@Input() fallbackComponent: any;
hasError = false;
errorMessage = '';
retry(): void {
this.hasError = false;
}
ngOnInit(): void {
// Listen for errors from child components
window.addEventListener('error', (event) => {
this.hasError = true;
this.errorMessage = event.message || 'An unknown error occurred';
});
}
}
Usage:
<app-error-boundary>
<app-complex-component></app-complex-component>
</app-error-boundary>
Real-World Example: API Error Handling
Here's a complete example showing how to handle errors in an API service:
// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry, timeout } from 'rxjs/operators';
import { User } from '../models/user.model';
@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(
timeout(10000), // Timeout after 10 seconds
retry(2), // Retry failed requests up to 2 times
catchError(this.handleError) // Then handle errors
);
}
getUserById(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`).pipe(
catchError(this.handleError)
);
}
createUser(user: User): Observable<User> {
return this.http.post<User>(this.apiUrl, user).pipe(
catchError(this.handleError)
);
}
private handleError(error: HttpErrorResponse) {
let errorMessage = '';
if (error.status === 0) {
// A client-side or network error occurred
errorMessage = 'Network error occurred. Please check your connection.';
} else {
// The backend returned an unsuccessful response code
errorMessage = `Server returned code ${error.status}, message: ${error.error?.message || 'Unknown error'}`;
}
// Return an observable with a user-facing error message
return throwError(() => new Error(errorMessage));
}
}
Using this service in a component:
// users.component.ts
import { Component, OnInit } from '@angular/core';
import { UserService } from '../../services/user.service';
import { User } from '../../models/user.model';
import { finalize } from 'rxjs/operators';
@Component({
selector: 'app-users',
template: `
<div class="loading-spinner" *ngIf="loading"></div>
<div class="error-container" *ngIf="errorMessage">
<div class="alert alert-danger">
<i class="fa fa-exclamation-circle"></i> {{ errorMessage }}
<button class="btn btn-sm btn-outline-danger" (click)="loadUsers()">Retry</button>
</div>
</div>
<div class="user-list" *ngIf="users.length && !errorMessage">
<h2>Users</h2>
<ul>
<li *ngFor="let user of users">{{ user.name }} ({{ user.email }})</li>
</ul>
</div>
<div *ngIf="!users.length && !loading && !errorMessage" class="no-data">
No users found.
</div>
`
})
export class UsersComponent implements OnInit {
users: User[] = [];
loading = false;
errorMessage = '';
constructor(private userService: UserService) {}
ngOnInit(): void {
this.loadUsers();
}
loadUsers(): void {
this.loading = true;
this.errorMessage = '';
this.userService.getUsers()
.pipe(
finalize(() => {
this.loading = false;
})
)
.subscribe({
next: (users) => {
this.users = users;
},
error: (error) => {
this.errorMessage = error.message;
console.error('Error fetching users:', error);
}
});
}
}
Summary
Effective error handling is a critical aspect of building robust Angular applications. We've covered:
- Global error handling with
ErrorHandler
- HTTP error interceptors for API calls
- Component-level error handling using try-catch and RxJS operators
- Form validation to prevent errors
- Custom error pages for specific error scenarios
- Creating error boundaries for component isolation
- A comprehensive real-world example
By implementing these error handling strategies, you can create more resilient applications that gracefully handle errors, provide meaningful feedback to users, and make debugging easier for developers.
Additional Resources
- Angular Error Handling Official Documentation
- RxJS Error Handling
- Angular HTTP Error Handling
- Angular Form Validation
Exercises
- Implement a global error handler that logs errors to a remote service.
- Create an HTTP interceptor that handles 401 Unauthorized errors and redirects to a login page.
- Build a component with error boundary-like behavior to isolate errors in complex UIs.
- Create a custom notification service that displays different types of error messages based on error codes.
- Implement a retry strategy that uses exponential backoff for transient errors.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)