How to treat Errors as first-class citizens in Flux (and Redux)
We’ve all done it. Sometimes we still do it. It seems intuitive and logical. It seems like a separation of concerns and a familiar way to handle errors, but is it the right way to do it?
That’s right, I’m talking about actions types:
FOO_SUCCESS, FOO_ERROR
According to the Flux Standard Action documentation, a standard put in place to help create a single standard for Flux Actions across libraries, Errors should be treated as first-class citizens.
The same people who brought us Flux Standard Action also brought us the Redux-Actions library, which many of us use, especially the createAction function.
What does it mean for an Error to be first-class
Treating an error as first-class means that we treat it like all other payload data in our actions. It means we don’t need a special-case action to denote when errors occur in our systems and we can instead respond to errors by knowing the type of the payload we’re receiving.
This is analogous to how we work with errors in Promises. That state of a rejected promise is part of the same concern, the Promise. What we don’t do is disambiguate the error from the success but rather we treat both responses as first-class data that can be acted upon.
Think about this from the perspective of an action-listener. You’re listening for an action, maybe you’re a reducer, perhaps you’re a saga. You want to know the state of the thing you’re listening to.
But wait, every time you receive an action of the type you’re interested in, it’s always a success. Why is that? How do you respond to errors in your system?
In the above case you also have to know about a separate action that denotes errors in your first action. This is the main reason FOO_SUCCESS and FOO_ERROR are an anti-pattern. They hide the knowledge about errors from your listeners and force them to know about other listeners of the same concern (splitting a single concern down the middle).
How do we denote errors then?
The FSA says two things about how errors should be represented by our Flux actions:
The optional error property MAY be set to true if the action represents an error.
If error has any other value besides true, including undefined and null, the action MUST NOT be interpreted as an error.
and then
By convention, if error is true, the payload SHOULD be an error object. This is akin to rejecting a promise with an error object.
The shape of our action begins to looks a little something like:
{
payload: Error || {},
error: true || false
}
Flux Standard Actions are only permitted to have four properties on them: type, payload, error and meta. I’ve heard a lot of confusion about why createAction always puts the action content inside a payload property and this is why.
Do I have to set the error property myself?
If you’re using the createAction function from Redux-Actions you’ll be happy to know that the error property is set for you based on whether or not the payload is an Error type.
How about Reducers, how do we handle actions now?
For the most part, reducers hardly change. If you’re using a switch to decide then you’ll need a new branching statement inside your case:
switch(action.type) {
case FOO {
return action.error ? b() : a();
}
}
If you’re using an if branch, you only need to change your FOO_SUCCESS and FOO_ERROR to read from the new error property.
if (action.type === FOO_SUCCESS) {
return a():
}if (action.type === FOO_ERROR) {
return b();
}
Becomes:
if(action.type === FOO) {
return action.error ? b() : a();
}
In most cases your code will become simpler.
Conclusion
Flux Standard Action defines a common language for Flux Actions. This reduces the cognitive load on developers when joining new projects or deciding how to shape your actions. These kinds of rules make life that little bit simpler by making some decisions.
By treating errors as first-class citizens we maintain a more correct separation of concerns. Now our reducers, sagas and anything else listening for our actions can determine for themselves what they want to do when the payload is an error rather than having to subscribe separately to a possibly non-existent FOO_ERROR action.
The ideas presented here for Actions are consistent with other asynchronous paradigms such as Promises. Having this common approach to handling errors, again, reduces cognitive load and makes our applications easier to reason about.
Thanks for reading. I hope this was interesting and if there are any questions, feel free to ask in the comments. Happy Sunday :)