Effective Error Handling in Angular: A Comprehensive Guide
As you rightly noted, errors are an inevitable part of any software system. They are, in a sense, a feature that reveals unexpected conditions or failures. The crucial difference lies in whether and how we manage them. Angular provides a layered approach to handle errors gracefully, ensuring application stability and a better user experience.
This guide is accompanied by a GitHub repository with practical examples to help you implement these concepts in your projects.
Synchronous Errors
Synchronous errors occur when code executes sequentially. Examples include incorrect use of a synchronous library, logical bugs in your code, or trying to access properties of an undefined variable, among others.
In JavaScript, and by extension Angular, the primary mechanism for handling these errors is the try...catch
block. Code that might throw an error is placed in the try block, and if an error occurs, the code in the catch block is executed.
Let's look at an example in buttons.component.ts
:
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; // Import CommonModule for @if
@Component({
selector: 'app-buttons',
standalone: true, // Assuming standalone component
imports: [CommonModule], // Import CommonModule if standalone
template: `
@if(error()) {
<p class="error-message">Error: {{ error()?.message }}</p>
}
<div class="buttons">
<button (click)="caughtError()">Caught Error</button>
<button (click)="unCaughtError()">Uncaught Error</button>
</div>
`,
styleUrl: './buttons.component.css'
})
export class ButtonsComponent {
error = signal<Error | null>(null);
/**
* Demonstrates a synchronous error caught using a try...catch block.
* The error is handled locally, preventing it from propagating further but f you want to handle it by the global error, you an re-throw it.
*/
onSyncSubmit() {
this.error.set(null);
try {
throw new Error('Sync error');
} catch (error) {
this.error.set(error as Error);
// Rethrow the error to let the error handler catch it
throw error;
}
}
/**
* Demonstrates a synchronous error that is intentionally not caught locally.
* This error will propagate up and eventually be handled by Angular's global ErrorHandler.
*/
unCaughtError() {
throw new Error('We did not catch this error!');
}
}
In this example, we have two buttons that trigger distinct methods:
- caughtError(): This method uses a try...catch block to gracefully handle an error. If you click the "Caught Error" button, the error will be displayed on the screen via the error signal, and your browser console should remain clean.
- unCaughtError(): This method throws an error directly without a local try...catch block. If you click the "Uncaught Error" button, the error cannot be caught within the component's scope, and you'll observe an error message appearing in your browser console:
This simple illustration demonstrates the critical difference: an uncaught error, like the one in unCaughtError(), will eventually propagate to Angular's ErrorHandler. This global handler acts as the ultimate destination for unhandled exceptions in your application.
Angular's Default ErrorHandler
At its core, Angular provides a default ErrorHandler class that functions as a global catcher for uncaught runtime errors. By default, this handler primarily logs errors to the browser console.
Here's a simplified representation of Angular's default ErrorHandler:
class ErrorHandler {
/**
* @internal
*/
_console = console;
handleError(error: any) { // Added 'any' type for clarity
this._console.error('ERROR', error);
}
}
While this default behavior is helpful for basic debugging during development, it falls short when it comes to providing a seamless user experience or integrating with external logging and monitoring services in a production environment. For a more robust solution, we need to implement a custom global error handler.
Implementing a Custom Global ErrorHandler
To enhance the user experience and gain more control over how errors are handled, we can implement a custom service that extends (or implements) the ErrorHandler interface and overrides its handleError method. This customized method will be invoked whenever an uncaught error occurs anywhere in your Angular application.
1. Create the Custom Error Handler Service
// src/app/services/error-handler.service.ts
import { HttpErrorResponse } from '@angular/common/http';
import { ErrorHandler, inject, Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
@Injectable()
export class CustomErrorHandler implements ErrorHandler {
readonly #snackbar = inject(MatSnackBar);
handleError(error: any): void {
// Here, you can implement your custom error handling logic:
// 1. Log the error to an external service (e.g., Sentry, LogRocket, custom backend)
// 2. Display a user-friendly message (e.g., using a MatSnackBar, a dedicated UI component)
// 3. Redirect to a custom error page
// 4. Handle each error depending on its type (e.g. HTTP Errors, or its code, 500, 404 ...)
if (error instanceof HttpErrorResponse) {
// Handle HTTP errors specifically if needed
console.error(`HTTP Error: ${error.status} - ${error.message}`);
// Example: Display a specific message for HTTP errors
this.#snackbar.open(`HTTP Error: ${error.status} - ${error.message}`, 'Close', {
duration: 5000,
verticalPosition: 'bottom',
horizontalPosition: 'center',
});
} else {
// Handle other types of errors
this.#snackbar.open(`An unexpected error occurred: ${error.message}`, 'Close', {
duration: 5000,
verticalPosition: 'bottom',
horizontalPosition: 'center',
});
}
console.error('Custom Error Handler:', error);
}
}
2. Provide the Service
To ensure your custom error handler takes precedence, you need to provide it at the application level, effectively replacing Angular's default ErrorHandler.
For standalone applications (Angular 15+), this is typically done in your app.config.ts file:
// src/app/app.config.ts
import {
ApplicationConfig, ErrorHandler, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { CustomErrorHandler } from './services/error-handler.service';
export const appConfig: ApplicationConfig = {
providers: [
provideZonelessChangeDetection(),
provideBrowserGlobalErrorListeners(),
{ provide: ErrorHandler, useClass: CustomErrorHandler },
MatButtonModule,
MatSnackBarModule,
]
};
Within the handleError method of your GlobalErrorHandler, you gain full control. You can perform actions such as logging the error to an external service (like Sentry or LogRocket), displaying a user-friendly error message using a snackbar or a custom UI component, or even redirecting the user to a dedicated error page.
This centralization ensures consistent error handling across your application, significantly improving maintainability and user experience. 🤗
You can always control a synchronous error at component level with the try ...catch block at rethrow it to allow the global error handler to perform any action in the background, like sending it to Sentry f.e.)
Browser Global Error Listeners: provideBrowserGlobalErrorListeners()
Angular also offers provideBrowserGlobalErrorListeners(), an environment initializer that forwards unhandled browser errors (like unhandledrejection and error events) to the ErrorHandler, further centralizing error capture. You should add it to the providers array in the ApplicationConfig object.
Top comments (0)