Introduction
In this article I'll tackle the challenge of building a robust error handling in our user profile form application. I'll look beyond simple validation errors and dive into a wider array of issues that can arise during the HTTP communication with the backend server. What if there's no network connection, or the server sends us an unexpected response? To ensure a smooth user experience, we need to anticipate these errors and provide clear feedback or recovery options.
What I'll cover
- The need for comprehensive error handling - look beyond validation errors and uncover into the different types of issues that can occur during HTTP communication.
- The power of interceptors - discover how interceptors can act as a central point for managing errors, validating responses, and enhancing security.
- Creating and registering an interceptor - the process of setting up an Angular interceptor.
- Validating successful responses - implement the logic to ensure that the server's 200 OK responses match our expected format.
- Handling network errors - learn how to detect and manage scenarios where the user loses internet connection.
- Tackling other errors - explore strategies for handling server-side errors and unexpected issues.
A quick note
- This article builds upon the concepts and code I've developed in previous articles in this series. If you're just joining us, I highly recommend catching up on the earlier articles to make the most of this one.
- You can find the code I'll be working with in the
16.user-service
branch of the repository.
Identifying the current issues
So far, I've focused on handling form validation errors within the tapValidationErrors
operator - those 400 Bad Request responses from the server when the form data isn't quite right. However, there are other types of errors that can crop up, and we need a way to deal with them too. These include:
- network errors - the "no internet connection" scenario.
- invalid response formats - even if the server responds with a 200 status code, the data might not be in the format we expect.
-
unexpected errors - the server could return various error codes, such as 4xx or 5xx, but other than the 400 Bad Request, which I already handled in the
tapValidationErrors
RxJS operator.
Current error handling limitations
Currently, error handling is primarily managed by the UserProfileComponent
, using the tapError
operator to set an error flag or display a popup message. Additionally, the tapResponseData
operator assumes the response will always be in the expected successful format. We need to expand our error-handling capabilities to cover unexpected scenarios and responses with invalid formats.
Introducing Angular interceptors
That's where Angular HTTP interceptors come into play. These handy tools let us intercept and handle HTTP requests and responses, giving us greater control over how our application communicates with the backend.
They allow us to:
- Catch errors globally - Instead of handling errors in every component, we can catch them in one place.
- Validate response formats - We can verify that server responses match our agreed-upon structure.
- Handle specific error types - We can differentiate between various error scenarios (e.g., network errors, authorization errors, server errors) and respond appropriately.
- Enhance security - We can add headers, tokens, or other security measures to requests and responses.
Creating and registering an interceptor
To get started, I'll create an interceptor in the src/app/core/interceptors
folder using the Angular CLI command ng generate interceptor server-error
. This will generate a file named server-error.interceptor.ts
:
Next, I need to tell Angular to use this interceptor whenever we make HTTP requests with the HttpClient
service. More specifically, I need to update the app.config.ts
file:
At this point, our interceptor doesn't do anything yet. My first task will be to validate the format of successful responses, which have the HTTP "200 OK" status code.
Validating successful response format
Recall that in the tapResponseData
operator definition I've assumed that successful responses from the server follow a specific format.
Let's recap that format defined within the server.ts
file. It's an object of this type {status: 'ok', data: any}
:
It's important to note that there is no single, universal standard for RESTful API response formats. Each application can have its own conventions. However, once a format is established, the client (our Angular app) should verify if the server's response complies with it. This helps catch unexpected errors or inconsistencies on the backend.
Implementing the response format validation
Here's the updated interceptor, with the check for the format of 200 OK responses:
The check200ResponseBodyFormat
function verifies if a response matches the expected format. The interceptor taps into the HTTP response stream, checking if the response is a 200 OK
and if it fails the format check. If so, it displays an error notification using MatSnackBar
and throws a custom error.
To see this in action, you can intentionally modify the server.ts
file to return a malformed response with a 200 status code (e.g., change status: 'ok'
to status: 'bad format'
). Then, restart the server and reload the application. The interceptor should detect this error and display the notification.
Check out the updated code
The updated code incorporating the changes made so far can be found in the repository at this specific revision. Feel free to explore the repository to see the full implementation details of the interceptor.
Handling network errors
What happens when the user loses internet connection? This is a common scenario that we need to handle gracefully to provide a good user experience. To catch network errors, I'll leverage the catchError
operator within the interceptor. This operator allows us to intercept errors in the HTTP request/response pipeline and take appropriate action.
Implementation
Here's how I'll modify the interceptor:
Remember that network error detection can be tricky, and this implementation is just one approach. Depending on your application's specific needs, you might need to adjust or expand this logic further.
How it works
- The
catchError
operator intercepts any errors that happen during the request. - The
checkNoNetworkConnection
function checks if the error looks like a network issue. This function examines the error object for missing headers, a zero status code, and other clues. - If it's a network error:
- Show a friendly message to the user ("No network connection").
- Log the error so we know it happened (for debugging).
- Set a flag
wasCaught
on the error to remember that the interceptor already handled it. - Re-throw the error. This is important! It lets other parts of the app know about the problem. For example, the
tapError
operator I created earlier can now use thatwasCaught
flag to avoid showing the same message twice.
- If it's not a network error, I just re-throw it, letting other parts of the app deal with it in their own way.
Updating the tapError
operator
To ensure that we don't display multiple error notifications for the same error, I'll update the tapError
operator to check the flag wasCaught
on the error object. This flag is set by the interceptor when it catches a network error.
Here is the updated operator:
Then, in the UserProfileComponent
, I have to update the request stream pipeline in the saveUserData()
method where the operator is used:
Check out the updated code
The updated code with these changes can be found in the repository at this specific revision.
Handling other error types
While the interceptor manages invalid 200 responses and network issues, other error scenarios can still arise during server communication. For a robust user experience, I need to address these remaining errors as well.
Implementation
I'll enhance the existing interceptor code to handle these additional errors:
While I won't cover authentication-specific errors (401, 403) in detail here (as these are typically handled by dedicated interceptors), it's important to have a strategy for dealing with unexpected server errors or other potential HTTP issues.
You might wonder, why re-throw the error after I've handled it in the interceptor? Here's the reasoning:
- flexibility - re-throwing the error allows for additional error handling at higher levels of our application; for instance, we might have a global error handler that logs errors or sends them to an error tracking service.
- component specific handling - our individual components might need to take specific actions based on the error; for example, our
UserProfileComponent
might want to display a more tailored error message in certain cases.
Skipping validation errors
One important thing to note is that I'm not going to handle validation errors in the interceptor. Why? Because I already have the tapValidationErrors
operator taking care of those. This operator is designed to catch errors that are related to the data we send to the server. The interceptor will let tapValidationErrors
do its thing and focus on other types of errors.
Removing tapError
from the component
Remember the tapError
operator I used in the saveUserData
method? We don't need it anymore. Since we're catching all errors in the interceptor and showing the appropriate messages, there's no need for the component to worry about error handling.
Checking Out the Updated Code
You can find the updated code incorporating these error-handling enhancements at the following revision.
Feel free to explore the repository, experiment with the code, and see how these changes improve your Angular application robustness in handling a wider range of HTTP errors!
Further resources on Angular interceptors
While I've covered the fundamentals of error handling with Angular interceptors, there's always more to learn. If you're eager to dive deeper into this powerful tool, here are some resources that will help you level up your skills:
- Angular 17 HTTP interceptors: A complete guide: This guide covers everything you need to know about Angular interceptors, from the basics to advanced use cases and best practices.
- Angular functional interceptors: Explore a modern, functional approach to creating interceptors, leveraging RxJS operators for clean and concise code.
- Angular's 17 interceptors: Complete tutorial: This tutorial provides a step-by-step walkthrough of building and using interceptors, with practical examples and explanations.
- What is an Angular interceptor and how to implement it? This article provides a beginner-friendly introduction to Angular interceptors, explaining their purpose and demonstrating how to create and use them in your applications.
Wrapping Up
In this article, we've leveled up our Angular user profile form by introducing an error-handling mechanism using an HTTP interceptor. I've tackled common challenges like:
- invalid response format - making sure the data we get from the server is what we expect, even when the status code is 200 OK.
- network errors - handling those "no internet" moments and giving the user helpful feedback.
- other server errors - catching and displaying messages for those unexpected server hiccups.
Check Out the Code
Ready to see it all in action? The complete code for this error-handling interceptor, along with the custom error classes and helper functions, can be found in the 17.error-interceptor
branch of the GitHub repository.
Feel free to explore, experiment, and adapt it to your own Angular applications.
Thanks for reading, and happy coding!
Top comments (0)