Understanding Angular Error Management: A Comprehensive Guide
Written on
Reliability is a fundamental aspect of system design, and Angular applications are no exception. These applications can encounter various types of errors, including logical, runtime, and network errors. When issues arise, they are referred to as faults. A fault-tolerant system is one that effectively manages these faults. This article will provide a comprehensive overview of error handling in Angular applications to enhance their reliability.
Let’s dive into the subject of error handling.
Expected Errors — Synchronous
The initial defense against errors in synchronous code is the try-catch block. This structure enables you to intercept potential errors within a specified section of code.
try {
// Error-prone code} catch (e) {
// Handle the error}
This method promptly captures any errors, preventing the application from crashing. However, caution is necessary, as this may not function as intended with asynchronous code.
try {
setTimeout(() => {
// Code that might throw an error
throw new Error("Something went wrong!");
}, 1000);
} catch (e) {
console.log(e); // This will not work}
In the example above, the catch block fails to capture the error since setTimeout operates asynchronously. The try-catch structure should solely encompass synchronous code.
setTimeout(() => {
try {
// Code that might throw an error
throw new Error("Something went wrong!");
} catch (e) {
console.log(e); // This will work}
}, 1000);
In larger applications, capturing every possible error with try-catch can be challenging due to numerous edge cases. So, how can we prevent crashes in such instances?
Global Error Handler
Errors that are not caught by try-catch blocks are escalated to Angular's Error Handler. By default, this handler merely logs the error to the console.
> The error handler serves as a centralized location for managing application exceptions.
However, if we want to prevent users from continuing when the application is non-functional, we can redirect them to an error page after logging the error. To accomplish this, we can replace the default error handler with a custom one.
To create an error handler, we need to implement the ErrorHandler interface.
import { ErrorHandler, Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'})
export class GlobalErrorHandler implements ErrorHandler {
handleError(error: Error): void {
// Implement this method}
}
This interface compels us to implement the handleError method, which will be invoked during an error occurrence. After logging, we can redirect the user to an error page within this method.
import { ErrorHandler, Injectable, inject } from '@angular/core';
import { Router } from '@angular/router';
@Injectable({
providedIn: 'root'})
export class GlobalErrorHandler implements ErrorHandler {
router = inject(Router); // Inject Router
handleError(error: Error): void {
console.error(error);
this.router.navigateByUrl('/error');
}
}
Next, we must override Angular's default error handler with our custom one. This requires providing it in our application module.
@NgModule({
...
providers: [
{
provide: ErrorHandler,
useClass: GlobalErrorHandler
},
],
...
})
export class AppModule { }
Expected Errors — Asynchronous
Angular leverages RxJS observables for managing asynchronous tasks. Similar to try-catch, we can also handle errors in observables.
Consider an example where an observable throws an error. If we fail to capture it, it will escalate to the global error handler, as previously noted.
ngOnInit(): void {
of(true)
.pipe(
tap(() => {
throw new Error('SOMETHING');})
)
.subscribe();
}
One way to manage this error is by using the RxJS catchError operator. This operator returns another error observable, which can be the same error, a modified version, or an empty observable to halt the error propagation.
of(true)
.pipe(
tap(() => {
throw new Error('Error');}),
catchError((error) => {
console.log("CAPTURED");
return EMPTY; // RxJS empty observable
})
)
.subscribe();
For instance, if we want to avoid navigating the user to an error page for specific errors, we can return an EMPTY observable to prevent the error from reaching the global handler.
We can also manage errors within the subscribe block by utilizing the error callback function.
of(true)
.pipe(
tap(() => {
throw new Error('Error');})
)
.subscribe({
next: () => console.log("Success"),
error: (e) => console.log("CAPTURED", e)
});
Global API Failures
Finally, we will address the handling of HTTP call failures. Since Angular employs observables for HTTP requests, we can capture these errors using the aforementioned methods, either with the catchError operator or within the subscribe function.
However, it is advisable to manage these errors centrally, similar to the global error handler. For example, we may wish to redirect users to a “server-error” page whenever an HTTP call fails.
We can achieve this using an HTTP Interceptor, which intercepts all outbound HTTP calls, allowing us access to the HttpRequest and HttpResponse.
To create an interceptor, we need to implement the HttpInterceptor interface and define the intercept method.
@Injectable()
export class ErrorResponseInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
// Continue with the request and capture the error
return next.handle(req).pipe(
catchError((err: unknown) => {
this.router.navigateByUrl('/server-error');
return EMPTY;
})
);
}
}
We use the same catchError operator since the HTTP request is also an observable. In case of an error, we navigate the user to the ‘server-error’ page and return an EMPTY observable to stop the error from propagating to the global error handler.
As with the global error handler, we also need to provide the HTTP Interceptor using the HTTP_INTERCEPTORS token offered by Angular.
@NgModule({
...
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: ErrorResponseInterceptor,
multi: true
},
],
...
})
export class AppModule { }
Conclusion
In summary, effective error handling is crucial in Angular applications, just as it is in any other software. We must address errors at multiple levels to ensure our application remains resilient and fault-tolerant. Gracefully managing errors is always preferable to allowing our application to crash.
If you found this article helpful, a clap would be greatly appreciated. If not, please leave your feedback!
Want to connect? [LinkedIn](#)
Feeling generous? [Buy me a Coffee](#) ??!