Error Handling with Angular Interceptors

cezar-plescan

Cezar Pleșcan

Posted on June 25, 2024

Error Handling with Angular Interceptors

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

  1. The catchError operator intercepts any errors that happen during the request.
  2. 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.
  3. 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 that wasCaught flag to avoid showing the same message twice.
  4. 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:

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!

💖 💪 🙅 🚩
cezar-plescan
Cezar Pleșcan

Posted on June 25, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related