DEV Community

Avalander
Avalander

Posted on

Handling errors with Either

An Either is basically a container for a value that might be an error. With an Either we can apply transformations to the contained value without having to worry whether it is an error or not until we reach a point in our code where we want to handle the error, should it have happened. It's a bit like a Schrödinger's box: the value might or might not be an error, we won't know until we open it (alright, missing the point of Schrödinger's cat, but I wanted to put the reference anyway).

How does Either work?

To illustrate the Either structure, let's build it in Javascript.

First of all, an Either can hold a value or an error. We'll call them Right and Left respectively. In a sense, it's like having two branches, and you go either to the left if you get an error, or to the right if you get a valid value.

Also, we need to be able to apply transformations to the value that is in the Either. Otherwise it's not really useful. We want a map function to do that. And we are going to apply the transformation only if we are on the Right branch, and ignore it if we have a Left.

const Left = x => ({
    map: fn => Left(x),
})

const Right x => ({
    map: fn => Right(fn(x)),
})
Enter fullscreen mode Exit fullscreen mode

Note that Left.map returns a Left holding the same value, without applying the transformation fn, while Right.map returns a Right containing the result of applying fn to the value. The reason for that is that we only want to apply the transformation on a valid value, not on an error.

Right(3).map(x => x * x) // -> Right(9)
Left(3).map(x => x * x) // -> Left(3)
Enter fullscreen mode Exit fullscreen mode

Now imagine that we want to apply a transformation to a value contained in an Either, but that transformation can return an error. Since we are handling error branches with Either, we might as well return a new Either.

const result = Right(3)
    .map(x => x % 2 == 0
        ? Right(x)
        : Left('Odd'))
Enter fullscreen mode Exit fullscreen mode

We have a number contained in an Either and we only want to accept even numbers. If it's odd, we return a Left saying that the number is odd.

The problem is that now we have a Left contained inside a Right. If we would inspect the variable result it would hold Right(Left('Odd')). If we want to apply another transformation, should we apply it to the outer Right or to the inner Left? What happens when the next transformation returns another Either?

To solve this issue, we can implement the method chain. chain is much like map, but it expects the transformation to return an Either, so it doesn't wrap the result of applying the transformation in a new Either.

const Left = x => ({
    map: fn => Left(x),
    chain: fn => Left(x),
})

const Right x => ({
    map: fn => Right(fn(x)),
    chain: fn => fn(x),
})
Enter fullscreen mode Exit fullscreen mode

Left.chain still doesn't apply the transformation, and it returns a Left holding the error, so we're sure we are not going to operate on an error should it have happened.

Right.chain will apply the transformation fn to the contained value and return the result, without wrapping it in another Right, because it expects the function fn to return an Either. If we were implementing this in a real project, we would probably want to check that fn returns an Either and throw an error if it doesn't.

We can use chain in the previous example to make sure that we don't end up with an Either inside another Either.

const result = Right(3)
    .chain(x => x % 2 == 0
        ? Right(x)
        : Left('Odd'))

result // -> Left('Odd')
Enter fullscreen mode Exit fullscreen mode

Now we only have a Left, and we would have a Right if our value had been even.

And that's it. We can use map to apply transformations to our contained value and keep it inside the same Either, or chain if we want to apply a transformation that returns another Either because it might fail.

Even though it's nice to be able to operate over a value without caring whether it's an error or not, it's not really that useful if we can't access the value. Right now the value is contained forever in an Either, and we will never know if the operation succeeded and the transformations were applied to the value, or if we have an error waiting to be handled.

We can implement one last method to solve this issue: fold. fold takes two callbacks, the first one (or left) will be called if the Either contains an error and the second one (or right) will be called if the Either contains a valid value.

const Left = x => ({
    map: fn => Left(x),
    chain: fn => Left(x),
    fold: (fnLeft, fnRight) => fnLeft(x),
})

const Right x => ({
    map: fn => Right(fn(x)),
    chain: fn => fn(x),
    fold: (fnLeft, fnRight) => fnRight(x),
})
Enter fullscreen mode Exit fullscreen mode

If we have a Left, fnLeft will be invoked, so we can handle the error in that function. If we have a Right, fnRight will be invoked and we can use it to send the value in an HTTP response, or store it in a database or do whatever we need with that value.

Right(3)
    .chain(x => x % 2 == 0
        ? Right(`${x} is even.`)
        : Left('Odd'))
    .fold(
        console.error,
        console.log
    )
Enter fullscreen mode Exit fullscreen mode

This simple example handles errors by printing them in console.error, and prints valid values in console.log, but we could handle errors and successes in any other way we need.

Handy Either factories

There are a few common factories for Either that we can implement easily.

Maybe

Maybe is a well known data structure, called Optional in some languages, that might or might not contain a value. We could model it with an Either that will be a Right if it has a value and an empty Left if it doesn't. Let's see how to build it.

const maybe = value =>
    (value != null
        ? Right(value)
        : Left())
Enter fullscreen mode Exit fullscreen mode

Or, if you don't like ternary operators that much,

const maybe = value => {
    if (value != null) {
        return Right(value)
    }
    return Left()
}
Enter fullscreen mode Exit fullscreen mode

TryCatch

Sometimes we might want to call a function that can throw an exception and treat the exception as an error with an Either. That might come in handy if we are using Either to handle errors in our code and need to interface with a library that handles errors by throwing exceptions (and expecting the user to catch them).

const tryCatch = (fn, ...args) => {
    try {
        const result = fn.apply(null, args)
        return Right(result)
    } catch (e) {
        return Left(e)
    }
}
Enter fullscreen mode Exit fullscreen mode

Conditional

We might want to check if a value fulfils a certain condition and return an error if it doesn't. We can define a factory that will take a predicate (i.e., a function that checks a condition on the value an returns either true or false) and a value, and return a Right if the condition holds true for the given value and a Left otherwise. We can get a bit fancier and allow an extra argument with an error value (usually a message explaining why the value wasn't accepted) that will be used if the value doesn't fulfil the condition.

const condition = (pred, value, reason) =>
    (pred(value)
        ? Right(value)
        : Left(reason))
Enter fullscreen mode Exit fullscreen mode

Remember the maybe factory that we implemented a bit earlier? Turns out that it's only a specific case of condition.

const maybe = value =>
    condition(x => x != null, value)
Enter fullscreen mode Exit fullscreen mode

When to use Either

My personal opinion is that Either is simply a strategy to handle application errors, and choosing this or another strategy is more a matter of preference that anything else.

Some languages, like Python or Java, offer a well-thought exception system that can be used to handle any application errors that might happen. In these languages it's usually a good idea to keep things idiomatic.

Other languages don't have an exception system and expect the programmer to return an error value if an error can happen in a function call (I'm looking at you, Go). Then I think it's better to use an Either than returning (err, result) and having to check for err every time we call a function, especially if we need to pass the error one layer up, where it can be handled.

And then there is Javascript. It has an exception system. Sort of. The problem is that catching specific errors while letting others propagate with Javascript's exception system is not a trivial task. Therefore it might be worth to use Either for application errors and leave exceptions for programming errors, instead of catching exceptions and trying to figure out if it's an error that should be handled here, elsewhere or make the application crash.

That's it, folks, thanks for reading!

Top comments (12)

Collapse
 
napicella profile image
Nicola Apicella

Hi! Nice article. I have been using vavr and its Either type for a while. I still didn't quite figure out how to use it when I need to expose different type of errors in the either. I thought about have different Error type and do a type assertion, but at this point I d better go with exceptions which expose this behaviour naturally(catch clauses with different types). Do you have any thought about it?

Collapse
 
avalander profile image
Avalander • Edited

Well, if you actually need different behaviour for different kinds of errors (as opposed to simply having different data in the error), you need a bit of extra effort to make it work with Either. I don't know about vavr, but it seems to be Java, and Java has a pretty decent exception system, so the easiest might be to just throw and catch checked exceptions.

That being said, Elm has a cool thing called union types which can be used to handle multiple kinds of errors. It's similar to an Either, but it allows more than two branches, each branch with a different behaviour.

To implement that in Javascript I would try something like this:

const UnionType = types => types.reduce((prev, type) => ({
    ...prev,
    [type]: (data) => ({
        match: (fns) => fns[type](data)
    })
}), {})

Then you can create your own union type to handle errors.

const CustomErrors = UnionType([
    'NetworkError',
    'InputError',
    'RandomError',
])

const someError = CustomErrors.RandomError({ message: 'Random' })

someError.match({
    NetworkError: ({ status, message }) => {...},
    InputError: ({ field, value }) => {...},
    RandomError: ({ message }) => console.error(message),
})

You can either have a union type in the left branch of an Either and do the matching when you fold it, or have a union type with a branch for valid values and several branches for invalid values (not sure how that second option would work out, though, you would need to implement map and chain in UnionType also). And, of course, you can use it for a lot of other things, besides handling different kinds of errors.

Now, this is something I just thought that might be interesting to borrow from Elm, but I haven't really tried in Javascript, so use it with caution.

Collapse
 
napicella profile image
Nicola Apicella

Interesting! Gotta read about union types and experiment a bit. Thanks a lot :)

Thread Thread
 
avalander profile image
Avalander

You're welcome, I hope you find it useful! :)

Collapse
 
normancarcamo profile image
Norman Enmanuel

Wow, it's good to see more people in FP these days!, more collaboration is always welcome.
Btw, how would you handle sync/async + composition?
It's a bit hard to handle all these concepts together in JS, in Elixir or Scala I don't see too much problem due to their nature.

Thanks!.

Collapse
 
avalander profile image
Avalander

Thanks for the comment :)

Btw, how would you handle sync/async + composition?

That's a good question. Unfortunately, I don't have a good answer.

Sometimes I just use promises as pseudo-either monads when I have to mix sync and async code and that's good enough.

// Instead of throwing an exception with invalid JSON,
// this function will capture it in a promise's rejection branch.
const parseJson = data => Promise.resolve(data)
    .then(JSON.parse)

// Another sync function wrapped in a promise.
const verifyResponse = ({ statusCode, body }) =>
    statusCode === 200
        ? Promise.resolve(body)
        : Promise.reject({ statusCode, body })

parseJson(someData) // For some weird reason our data is stringified JSON, very convenient to manipulate.
    .then(postData) // We send our data to a remote host
    .then(verifyResponse) // We verify that we get 200 back
    .then(parseJson) // We parse the body of the response
    // ...

Another option would be to map eithers and maybes to promises when composing sync and async functions.

const Left = x => ({
    ...
    toPromise: () => Promise.reject(x),
})

const Right = x => ({
    ...
    toPromise: () => Promise.resolve(x),
})

const map = F => x => F.map(x)
const chain = F => x => F.chain(x)
const toPromise = F => F.toPromise()

const parseJson = data => tryCatch(JSON.parse, data)

const verifyResponse = ({ statusCode, body }) =>
    statusCode === 200
        ? Right(body)
        : Left({ statusCode, body })

parseJson(someData)
    .toPromise()
    .then(postData) // We send our data to a remote host
    .then(verifyResponse) // We verify that we get 200 back
    .then(chain(parseJson)) // In case you want to keep the Either inside the promise
    .then(toPromise) // In case you want to unwrap the Either

Most of the times I think promises as pseudo-eithers is good enough, so I haven't explored how to compose them with proper eithers that much.

Collapse
 
normancarcamo profile image
Norman Enmanuel

Thanks for reply!

I actually used to do what you mention in the first code example, but not in the last example (nice trick to explicitly and descriptively passing from sync to async), at the end of the day when we enter to the async world we cannot exit, even worse, they can be nested, but luckily we have async/await mechanism to flat it.

By the way, I think the operator pipe will fix this.

Collapse
 
christopheriolo profile image
Christophe Riolo

This looks actually very much like monads, do you know about them? :) en.m.wikipedia.org/wiki/Monad_(fun...

Collapse
 
jvanbruegge profile image
Jan van Brügge

Either is a monad. As well as Maybe :)

Collapse
 
christopheriolo profile image
Christophe Riolo

I did not know Either specifically thank you for clearing it :)

Collapse
 
avalander profile image
Avalander

Yep. I didn't use the M word because I didn't want to scare people, but monads are cool. Thanks for bringing it up :)

Collapse
 
christopheriolo profile image
Christophe Riolo

Indeed I did not see the tag, I wasn't sure because of the absence of the usual vocabulary /)