Error Types
Since nowadays more and more logic is outsourced from the back end to the front end, the probability increases that a faulty behavior of the user leads to an unforeseen error, which can cause the application to “crash”. Now a web application cannot really “crash” like a desktop application, which in the worst case simply closes. No, the web application remains open in the current browser tab, only its behavior is no longer comprehensible to the user in case of an error. Since JavaScript is single-threaded in the browser, it can happen that a part of the interface freezes and an action is not performed correctly. In this case we speak of a local front end error. Since we as the developers don’t know where and when such an error could occur, it is important to catch all occurring errors at a central location.
Another error case that can occur is when a request to the back end fails and the front end receives an error message from the back end. Although in this case it is clear that the error is coming from the back end, there is a need to take care of the error handling for every single request to the back end. Again, it is better to handle these errors in a centralized location so that the user is presented with consistent error messages and also to avoid forgetting to intercept errors.
Example application
In the following we will look at an example application that uses global error handling and go through it step by step. A click on the first two buttons produces an error — the first a local front end error, the second via a bad response from the back end. The third button shows a successful request, where no error message is displayed, but only the loading spinner until the request is completed.
Application architecture
Before we turn our attention to the implementation details, we should also take a look at the structure of our Angular application. In a more complex application it is worthwhile to divide certain functionalities into different modules. For example, as in our case, there is a core module that contains the functionality that is globally available to the whole application and is also loaded immediately when the application is started — such as the error handling.
Another typical module is the shared module. As the name suggests, this module contains functionality that can be reused in several other modules of the application. These are mostly stateless user interface components (or so-called dump components), such as loading spinners or dialogs, which can be controlled by a service. In our case there are two dialogs, one for displaying the error message and one for displaying a loading spinner, which is displayed for the duration of a request.
File structure
In the following a simplified structure of the files is shown. You can directly see the separation into core and shared modules. In this case, the shared directory contains the components and services needed to display the error messages and the loading spinner. The core directory in turn contains the files for the interceptors that globally intercept and process the errors in the application. Both the core module and the shared module are imported into the app module, which is usually the entry module of an application in Angular.
📦 src
┣ 📂 core
┃ ┣ 📂 errors
┃ ┃ ┣ 📜 error-handler.module.ts
┃ ┃ ┣ 📜 global-error-handler.ts
┃ ┃ ┗ 📜 http-error.interceptor.ts
┃ ┗ 📜 core.module.ts
┣ 📂 shared
┃ ┣ 📂 errors
┃ ┃ ┣ 📂 error-dialog
┃ ┃ ┗ 📜 error-dialog.service.ts
┃ ┣ 📂 loading
┃ ┃ ┣ 📂 loading-dialog
┃ ┃ ┗ 📜 loading-dialog.service.ts
┃ ┗ 📜 shared.module.ts
┗ 📜 app.module.ts
Error Handler Module
The Error Handler module is the entry point for the global Error Handler. It is part of the core module and registers two providers. The first one is responsible for the general error handling, which catches all errors occurring within our application. The second provider is an HTTP interceptor, which is called for every interaction with the back end. The multi
property must always be set to true
in this case, since the HTTP_INTERCEPTORS
injection token can potentially be assigned to several classes.
Global Error Handler
As you have seen in the last section, the GlobalErrorHandler
class was registered as provider in the Error Handler module. This class implements the ErrorHandler
class and contains a handleError
method. This method is called whenever an error is thrown somewhere in the application. The error is passed as a parameter and can be processed further inside the method. In our case a dialog is opened where the error message should be displayed and the error is logged to the browser console.
The opening of the dialog takes place in a callback of zone.run
, so that the dialog window can be closed even if the error is thrown outside the ngZone. This is for example the case if an error occurs in a lifecycle hook like the ngOnInit function in a component.
In our example there is a method in the AppComponent
called localError
which throws an error:
localError() {
throw Error("The app component has thrown an error!");
}
So, if the first button in the example is clicked it will call this method and the GlobalErrorHandler
processes the thrown error by showing this dialog:
The user can then click on the “Close” button to close the error dialog and continue using the application. Of course, the processing of an error may vary from case to case and require further steps. For example, for monitoring purposes, it can be useful to write the error message to a log file in the back end, to navigate the application to another page or to reset a certain state. In this example we simply show the error dialog.
HTTP Error Interceptor
The interception of HTTP errors is done by an HTTP Interceptor class. The HttpErrorInterceptor
class implements the interface HttpInterceptor
by providing the method intercept
. This method is called automatically on every HTTP request that is made through our application — regardless of whether it was successful or not. This also allows us to set up a loading spinner. The first thing we do is to call the service for the Loading Spinner dialog to display it:
this.loadingDialogService.openDialog();
The dialog for the loading spinner will be opened and lets the user know that an interaction with the back end is currently taking some time:
After this the processing of the HTTP request is executed which is basically done with an HTTP handler called next
. The HTTP handler provides ahandle
method that processes all HTTP requests by returning an RxJS observable. Based on this observable the pipe
method can be called which provides us the possibility to use some RxJS operators to handle the requests one after each other.
Here we’re using the operator catchError
that is executed when an error is thrown because of a bad request to the back end. As a first action the error is simply logged to the console in the web browser. At next, the service for the error dialog is called to open the dialog with the error message and the status code of the error response (e.g. 404). Finally, we have to return the emitted error notification by calling throwError
with the error as argument.
With the help of the finalize
operator the dialog of the loading spinner is hidden again, regardless of whether the request has thrown an error — i.e. catchError
was called — or not.
In our sample application you can see that the following HTTP request does not need any additional error handling:
failingRequest() {
this.http.get("https://httpstat.us/404?sleep=2000").toPromise();
}
The error dialog will look like this:
Finally, it should be noted that the global error handler also became aware of the HTTP error (you can see this in the browser console). In this case, however, no separate dialog window for the error was opened, because the service for the error dialog was already triggered by the HTTP error interceptor before and in this case prevented a second dialog from being opened at the same time. In general you can find some examples on the net, which use the instanceOf
operator within the global error handler to distinguish if it is an HTTP error or not. In this case, the entire error handling — including the HTTP errors — would be handled by the global error handler. With the current Angular version (v. 10) this is not possible because both error types are simply recognized as “Error” by instanceOf
and there is no further information about whether it is an HTTP error or not. Therefore this article shows an alternative approach to achieve the same error handling but with having it separated into the error handler, which catches everything and an interceptor, which only takes care of the HTTP errors.
No comments:
Post a Comment