RxJS state management in Angular 2 + Error handling.
TL;DR: Curious about error handling? Scroll down to the final approach and/or check out the example project.
Angular 2 is a great framework that provides nice tools to create awesome component-based apps. When it comes to state management, Angular decided not to reinvent the wheel and lets its users choose how to manage state. This post will go through a couple of ways to neatly manage your data in an Angular 2 (TS) app and handle errors in data streams.
In the “old days”, a few years ago up until recently, promises were used to fetch data from the server and usually stored in a service or controller. Sharing this data was achieved through polling/dirty-checking mechanisms. A conventional way of realizing this was by creating a service that returns http promises that would be called and handled by controllers.
The problem with this approach in a component-based application is that multiple components might be interested in this data at different times. To prevent a fetch to the server every time the data is needed, local state is held by a service. However, updating this data causes problems for components that contain a copy of the state of the service. The component that calls this “get” function would be the only one that is up-to-date, whereas other components would have to watch (poll) the state. Gross.
Angular 2 + RxJS
RxJS is a great tool for managing data with help of the Observer pattern. Instead of keeping state in a variable, it stores data in a stream. For this post, I will not go into the details of what kind of streams/operators RxJS provides.
Angular decided to include RxJS as a direct dependency of the project, so you have access to it out-of-the-box. Promises are replaced by Observables, so we can dive into it immediately.
Let’s transform the AngularJS example to Angular 2.
This piece of code contains exactly the same problem as the AngularJS example. The only difference is that we upgraded the framework and transformed the Promise to an Observable. Let’s have a look at the diagram provided in NgBook 2.
- A service action fetches data from the API
- The result of the API call gets put into the Data Store (observable stream)
- All components (subscribers) are notified with new data
Let’s fix the code example so it fits the diagram.
The service contains a subject called “messagesSubject” that acts like a producer and observable. This subject is private because this service is the only one allowed to produce new data for this stream.
The data of the stream is exposed to the outside world by a separate variable “messages$” that contains only the observable part of the subject. This makes sure that components can’t mess with the contents of this stream.
You might think this is a lot of overhead to perform a simple http request, but the benefits will grow as your application (component tree) grows. This structure automatically updates N amount of components by only calling 1 action (1 request to the server). Farewell nasty poll mechanisms!
So far this has been pretty straightforward, but it gets tricky. Since I dove into Angular 2 during one of its later alpha releases, I’ve been struggling with error handling of streams. I’ll take you through the mistakes I made along the way and how I got to my personal solution. If you’re in a hurry and don’t like to read, jump to the final approach to see my personal favorite way of handling errors.
A RxJS subject (producer) contains 3 ways of updating a stream:
- subject.next(value); Pushes a new value through the stream and calls the first handler of a subscriber.
- subject.error(error); Pushes an error through the stream and calls the second handler of a subscriber.
- subject.complete(); Completes the stream and calls the third handler of a subscriber.
First approach (mistake)
As mentioned above, subjects contain this error function. A Http call can succeed or fail, meaning there can be a value or error. Great! (Or that’s what I initially expected with limited knowledge of RxJS streams).
After hooking up some components to this stream and letting a Http request fail, it seems like it works as expected. Unfortunately it’s not that easy. Calling the error function of a stream closes the stream. This means that after handling the Http error, performing another action will not do anything! The components are all decoupled from the stream because the stream is closed.
We don’t want streams to ever close, at least not in the context of Angular 2 services. So never manually call the error or complete of a subject you don’t want to close!
Second approach (works)
A pretty straightforward solution would be to create a messagesErrorSubject, which does exactly the same as the messagesSubject except for errors instead of messages. This approach splits the happy flow from the error flow, which gives the flexibility to only listen to errors or messages.
Speaking of overhead.. Imagine a CRUD service: The happy flow will contain 4 streams for Create, Read, Update and Delete + 4 streams for their error equivalents. This equals 8 streams in 1 service!
Third approach (works)
To minimize the amount of streams, we should go back to the basics of how we used to handle Http responses. It’s possible to get data or an error, so why not both in 1 stream? (I’ll tell you later why you shouldn’t).
As seen in the example above, streams are declared with 2 types, either the happy flow data type or the error response type. This approach cleans your service, but moves some logic to components which is not desired as components now have to filter streams to see what it should do.
To improve upon this solution, it is possible to create custom classes so you can instantiate your error in the stream to filter on more specific types. The problem with this approach is that on componentlevel filtering is required. I personally am not a fan of this solution since it makes the services less readable and puts more responsibility in the components.
Fourth approach (final)
This is my personal favorite and the one I use in my own applications. It’s a combination of the 2nd and 3rd approach. In this approach, all the streams are happy flow only. This means that the data stream only acts on successful data emissions. The errors will go through an error stream which can be filtered on component level.
For this example we’re going to implement a CRUD service that contains 5 streams:
Cool, we have 4 happy flow streams that notify the application that the CRUD operations succeeded. The error stream has a special type, HttpError. Let’s take a look at that class.
This abstract class is the base of our CRUD error handling mechanism. For each operation a new implementation of this base is required. For the sake of this example we’ll implement read only.
That’s it! Now we’re ready to implement the read method of our CRUD service.
Now we have data streams that never close, give expected results, and scale with limited overhead.
How does this look on implementation level? Good question.
With this setup, N amount of components can either subscribe to successful operations, failed operations or even both. Through the action-based state structure, an action can be emitted by your smart component and your whole application is up-to-date!