Handling action errors in Redux-Saga using Either

This was the first image that came up for a Google search of “either” and I thought, yeah, that’s freakin’ spot on.

Handling errors in redux-saga has been something we’ve explored from time-to-time but never found a solid solution.

We’re also fans of Flux Standard Action (I’ll refer to these as FSA from here on) which means that we treat errors as first class citizens. I’ve written more about that here: https://medium.com/@jamiedixon/how-to-treat-errors-as-first-class-citizens-in-flux-and-redux-fca7f3d5c02d#.twl75pyg0

The problem

FSA actions come in two main states, their happy path “OK” state, and their “Error” state when payload is an Error type.

Because we choose to use FSA it means we have to (remember) check if action.error is true before doing any work in our reducers/sagas.

The problem is that we often forget and while we’re churning away to get features complete, the error states get missed and code is executed on actions that cause problems.

There’s also the overhead that every saga listening for any action must check action.error and since we can’t always know what’s going to dispatch our action, we have to assume it could come from anywhere within our large application, including from other sagas.

The solution

One solution is to make sure every reducer and saga checks action.error before doing any real work. Most of our sagas are only interested in non-error action payloads which leads to a lot of

if (action.error) return;

We need a more generalised approach that allows us to

  1. Run our main happy path code when there’s no error state.
  2. Catch errors in the action and do something useful if we so wish.
  3. Forces us to be explicit about which code is which and doesn’t let us get away with not being clear.

Considering this, one data structure in particular springs to mind that could help us solve the problem and that structure is Either.

When I first had the idea I checked to see if anyone else had also thought of it and being the internet, and most developers being a bunch smarter than me, of course they had. It’s not unique or novel, but it does deserve a bit of a write up.

Either is a type with two sub-types Left and Right. In this example we don’t actually need a main type called Either we just need the two sub-types, so if you’re coming from a typed language background you can think of Either as the Type of both Left and Right, or even as an interface if you’re an experienced OOPer.

Both Left and Right expose the same interface which in our case will include map and fold.

A bit of an explanation of Either: The Either type refers to an interface that will be used to handle two different results from a single operation. If the result of this operation is one thing, we’ll return a Left and if it’s another thing we’ll return Right. It’s either this, or that.

In our case we want to denote an “Error” result vs an “OK” result and so we’ll use Left for our error and Right for our OK.

The power of Either

Because both Left and Right adhere to the same interface, we can guarantee that the functions map, fold will exist. Our implementations of these functions on the these two types is what will make all the difference.

Let’s start with map. map is a function that takes a function and returns a new instance of the container that the function was called on. Consider Array.prototype.map, it takes a function, and returns an array. In this case the array is the container type that the function was called on. map is a way of taking the value out of the container, performing some operations on it, and then putting it back in the container before returning it.

So both of our Left and Right types will have a map function that takes a function and returns a Left or a Right. Here’s where the two types diverge. While both Left and Right will have a map function, we only want to execute the function passed to map when we’re dealing with Right, that is, an OK result. If we’re dealing with Left we want to ignore the function passed to map and instead just return a new Left.

Let’s write a quick implementation of these two types:

function Left(value) {
return {
map: () => Left(value)
}
}
function Right(value) {
return {
map: (fn) => Right(fn(value))
}
}

So there we have our map. When we’re dealing with a Left we ignore the function passed in and when we’re dealing with a Right we execute the function passing the value. In both cases we re-box the resulting value in a new Left or Right.

Reduxifying

We’ve defined two types; Left and Right. These correspond to our two states; “Error” and “OK”.

Let’s go ahead and write out the use-case for our Either types. Ideally our reducers and sagas would receive an Either type that they can work with. Let’s for a moment imagine that the payload from an action is an Either type. Given that case, here’s our before and after code:

Before

function* productsReceivedSaga({ error, payload }) {
if (error) {
return;
}
/* Do the real work */ 
}

After

function* productsReceivedSaga({ payload }) {
yield payload.map((p) => {
/* Do the real work */
});
}

This isn’t quite going to cut it though. Firstly, our map function will return an Either type but in order to correctly yield the values up to the redux-saga middleware we’re going to need to “un-box” the value back out.

This is where fold comes in. fold is a function that takes a function, executes it with the value contained in our Either and returns a plain, unboxed result.

function Right(value) {
return {
map: (fn) => Right(fn(value)),
fold: (fn) => fn(value)
}
}

So now we have a way of taking the value back out of the Either and doing something with it. Our saga can fold the value back out of the Either using the identity function x => x.

function* productsReceivedSaga({ payload }) {
yield payload.map((p) => {
/* Do the real work */
}).fold(x => x)
}

Redux-saga middleware will now receive any yielded code produced inside our map.

Working with errors

We also stated that we want a way of dealing with errors that arise and when they do, we’ll be working with a Left type.

To hand over control to our error processing code only when there’s a problem we can pass a second parameter to our fold function which will be executed instead of the first function. We’ll call the initial function passed to fold _ to indicate that we’re not using it when we’re a Left type.

function Left(value) {
return {
map: () => Left(value),
fold: (_, fn) => fn(value)
}
}
function Right(value) {
return {
map: (fn) => Right(fn(value)),
fold: (fn) => fn(value)
}
}

We can use this in our saga:

function* productsReceivedSaga({ payload }) {
yield payload.map((p) => {
/* Do the real work */
})
.fold(
x => x,
err => /* error handling here */
)
}

Now we need a way of wiring this up in redux so our reducers/sagas are given the correct Left or Right type.

Redux middleware

The middleware for this is fairly simple. We want to inspect our action and if there’s an error property set to true then we return a Left(payload) and if there’s no error then we return a Right(payload).

Let’s write a function that takes an FSA action and returns us the right type:

function makeEither(action) {
return action.error ? Left(action.payload) : Right(action.payload);
}

and viola! we have everything we need to piece together a simple redux middleware that converts payloads into an Either type:

export default store => next => action => next({
...action,
payload: makeEither(action)
});

Recap

We wanted a way to run our reducer/saga code when the payload of our action isn’t an error, and we didn’t want to have to write boilerplate code to check every time we receive an action.

We also wanted the choice to run code when an error payload is received.

By using the Either construct with a bit of redux middleware we’ve ensured that everything inside map will only run when we’re in an OK state and that fold will run one of two functions depending on whether we’re a Left or a Right type.

These are ideas that go way-back and I’m by no means the first to think of applying them to redux and redux-saga. It’s my hope that by writing my thoughts here there will be a useful reference for those looking into similar ideas.

Thanks for reading!

Edits

13/03/2017: The original articles used a function namedcatch instead of the second parameter to fold. I wasn’t keen on this diversion from the usual fold function and decided to change it.