In this application that uses a public API to search for beers, we're already handling things like debouncing the user's input and canceling pending requests should we need to make another. One thing it can't do yet though is handle network errors gracefully. Let's put some code in place to simulate a flaky network.
In this example, we are destructuring the payload from the action that comes out of Redux, and we're passing it along to this Ajax helper function we have here. We take that search term, and we're just generating an RXJS Ajax observable. A quick and easy way to fake an error here would be to say that for certain search terms, throw an error and for all others, return this regular Ajax observable.
Why don't we say if term is equal to Skol, then we'll return a throw observable? We'll say observable.throw. We'll pass through some error message. Otherwise, for all other search terms, just return the Ajax as normal. This will now allow us to type into that search input field and only when we type exactly the word Skol will it throw this error.
Let's give it a shot in the browser. Now you see if I type S-K, I'll get a regular search result. If I complete that error string, we get a thrown error. This happens because, in RX, errors bubble up just like they would with regular try-catch blocks. If you don't have anywhere in your code in which you are catching those errors, it will just throw them in the global context. We need to be careful about this.
The rule typically is anything that talks to the outside world you should expect that it could throw an error, and you need to handle that gracefully. Sometimes you'll just handle that in some kind of logging service, but most of the time with UI development, you're going to want to present the error to the user or at least explain to them that something failed.
Looking back at our application, let's implement a feature where when the search throws an error, we put a message underneath the search bar. Crucially, we need to allow the user to continue searching for other terms.
This is the function that generates either the error or the Ajax request. When it's successful, we call received beers which generates the success action to be dispatched back into the Redux store.
We want to handle the error in a very similar way. Should an error occur, we'll want to stop that error from balling any further up into the application. We also want to dispatch an error action into the Redux store. This will allow us to give the message to the user on-screen.
Let's first create the action that will handle this. We'll create another constant, and we'll say that this is the error case. We'll copy paste its function. This time, we're going to pass through an error object. The payload that we want to receive in the reducer is just the message. We can say error.message. Now we have this function to call when an error occurs.
Back in the Epic, we can add a catch after here. In here, we have access to the error, but we must return an observable. We can do return observable of, and then we'll call out to the action we just created, pass through the error, make sure we import it.
This now will achieve both of our goals. Having the catch here will stop the error propagating any higher in the application, and because the catch operator in RX allows us to return an observable, it means that we can produce a stream that just has a single action in it, this time being our error. That means that we can react to that in the reducer and set a message for the user.
Let's look in the browser to see how far we are so far there. Now if we repeat the steps from before, so we'll type S-K and get a regular search, then we'll complete the error string. You can see whilst we don't have the message yet, the application is still operational although we've got this loading spinner here which we'll deal with in a moment. If we delete a character, you can see that the application is still responsive.
If we open up the Redux dev tools, we can see the point in which the search beers error occurred. If we have a look at the action, you can see we get the message. This message is coming from here. Notice that after it occurred, other actions were still able to fire. We're in a really good position now. All we need to do is show a message to the user and get rid of that loading spinner should an error occur.
Now that we're firing this action every time an error occurs, let's go into the reducer and handle it. Let's copy this, and we'll say we're interested in when there's an error, import that constant. When an error occurs, we'll set loading back to false. Then we'll also go in to add some messages.
Messages will just be an array of objects that have a type. In this case, we're going to say it's an error. Then they'll have a text property which is the actual error itself. In our case, we know that's coming through on the action.payload. Now we want to have an empty messages array always in place so that we can do things like check its length when we're rendering other components. We'll add messages here to the initial state.
Then we'll want to remove any messages every time we do a search. Here we can just set messages back to an empty array. That will handle a search and the errors. Finally, when we receive beers, we'll just spread in the state here to ensure we're not overriding any other properties.
Now let's go ahead and use these messages that we're saving on the state. Back in the app file, we're bringing in the search component. Let's pass through the messages from the state also. We can say this.props.messages. Then inside the search component, we can bring in messages here.
Then just below the input, we'll check that messages has a length greater than zero. If it does, we'll execute this expression which is just a list that will render an LI for every message. We're using the type here to have a CSS modifier class so that we can change the message to be a red color. Then we're just rendering out the text.
Now, back to the browser. To see an action once more, we'll type a few characters, and then we'll cause the error. Now you see we get the error message here and the loading spinner has gone. Then we can delete a character. The error message will go away and the application continues to work as we expect it to.
One final note is that the type of errors that come back from external services are not usually what you want to display directly to the user. In that case, you could simply return your own error here. You can have them pre-defined in other files, etc., etc.