Enable javascript in your browser for better experience. Need to know to enable it? Go here.
Blogs Banner

The Either data type as an alternative to throwing exceptions

Exceptions are a mainstay of programming languages. They are commonly used to handle anomalous or exceptional conditions that require special processing, breaking out of the normal flow of the application. Some languages, such as C++ or Java, use them liberally. But not every language follows that design. C# or Kotlin don't have checked exceptions. Others, such as Go and Rust don't even have exceptions at all.

I find code that throws an exception every time something unexpected happens hard to understand and more difficult to maintain. In this article, I want to talk about using the Either data type as an alternative way of dealing with error conditions. I will be using Kotlin for my examples, as I feel the syntax is easy to follow. These concepts are not unique to Kotlin however. Any language that supports functional programming can implement them, one way or the other.
 

Different types of errors

When using exceptions you have to differentiate between the origin of the error, as they are not all equal. Some errors such as NullPointerException, or ArrayIndexOutOfBoundsException indicate bugs. Other errors are part of the business logic. For instance, a validation failing, an expired authentication token, or a record not being present in the database.

I want to talk about the latter. While not being part of the happy path, you still have to decide what to do whenever it happens. Moving the error handling out of the regular code and into some external handler makes it less explicit, as you can only see part of the implementation at a glance. You need more context to understand what's going on. 

In essence, exceptions are hidden goto statements, which are widely considered a lousy idea. The flow of the program is broken whenever an exception occurs. You land on an unknown point up in the call chain. Reasoning about the flow becomes harder, and it is more likely that you will forget to consider all the possible scenarios.
 

What does this usually look like?

Let's use a backend service that provides a REST API as an example. A simple architecture looks like this: a thin controller calls a service, which in turn calls a third party API — this looks testable and straightforward. Until you start dealing with error conditions.



If we don't handle the exception, it will bubble up until our API returns a 500 error to the caller. We don't want that.

A typical pattern in the SpringBoot ecosystem to use an exception handler. You put this method in the controller and it catches exceptions that happen in the chain of calls to services and APIs. Our caller then gets the error formatted the way we want.
 
@ExceptionHandler(JWTVerificationException::class)

fun handleException(exception: JWTVerificationException): ResponseEntity<ErrorMessage> {

    return ResponseEntity

      .status(HttpStatus.BAD_GATEWAY)

      .body(ErrorMessage.fromException(exception))

}
 

When exceptions make the flow harder to understand

Let's say that our service from the diagram above is going to verify JSON Web Tokens. The idea is simple. We are getting a JWT as a string, and we want to know if it is a valid token. If so, we want to get specific properties that we will wrap in a TokenAuthentication. This interface defines it:
 
interface Verifier {

    / **

     * @param jwt a jwt token

     * @return authentication credentials

     * /

    fun verify(jwt: String): TokenAuthentication
}
 

A signature that doesn’t quite tell the truth


If we dig into the implementation of the Verifier, we will eventually find something like this:
 

/ **

 * Perform the verification against the given Token

 *

 * @param token to verify.

 * @return a verified and decoded JWT.

 * @throws AlgorithmMismatchException 

 * @throws SignatureVerificationException

 * @throws TokenExpiredException

 * @throws InvalidClaimException

 * /

public DecodedJWT verifyByCallingExternalApi(String token);

As previously mentioned, Kotlin doesn’t have checked exceptions. As a consequence, however, the signature of Verifier is lying to us! This method might throw an exception. The only way to discover what’s happening is by looking at the implementation. The fact that we have to inspect the implementation to pick this up is a sure sign that encapsulation is lacking.
 

The explicit approach

There are two things I want to change in the Verifier implementation.

1. The verify method shouldn’t throw an exception.
2. The signature of the method should reflect that an error might happen.

We could use a nullable type; verify would then return a TokenAuthentication?. But it has a fatal flaw: We’re losing all the information about what actually went wrong. If there are different causes for the error, we want to keep that information. 

Enter Either (dum dum dum...).
 

The Either data type

Before we talk about Either, what do I mean by data type? A data type is an abstraction that encapsulates one reusable coding pattern.



In our case, Either is an entity whose value can be of two different types, called left and right. By convention, Right is for the success case and Left for the error one. It’s a common pattern in the functional community. It allows us to express the fact that a call might return a correct value or an error, and differentiate between the two of them. The Left/Right naming pattern is just a convention, though. It can help people who have used the nomenclature in existing libraries. You can use a different convention that makes more sense for your team, such as Error/Success, for instance.

We can create a simple implementation using sealed classes. We’ll see below how that can be combined with a when expression to make the code cleaner and safer at the same time.
 

sealed class Either<out L, out R> {

    data class Left<out L, out R>(val a: L) : Either<L, R>()

    data class Right<out L, out R>(val b: R) : Either<L, R>()

}

fun <E> E.left() = Either.Left<E, Nothing>(this)

fun <T> T.right() = Either.Right<Nothing, T>(this)


Let's rewrite our code to make use of Either.
 

Adapting the interface


The Verifier class now returns an Either type to indicate that the computation might fail.
 

interface Verifier {

    / **

     * @param jwt a jwt token

     * @return authentication credentials, or an error if the validation fails

     * /

    fun verify(jwt: String): Either<JWTVerificationException, TokenAuthentication>

}


Note that we’re still using the exception class to signal an error, but we aren’t throwing it anymore. Representing that exception as a sealed class can make handling all the possible errors easier for the client, as you can be sure that every condition has been accounted for.
 

Wrapping the code that throws

Inside our implementation of Verifier, we’re wrapping the problematic code with an extension method called unsafeVerify. We use the extension methods that we defined above to create both sides of an Either:
 
private fun JWTVerifier.unsafeVerify(jwt: String): Either<JWTVerificationException, TokenAuthentication> = try {

    verifyByCallingExternalApi(jwt).right()

} catch (e: JWTVerificationException) {

    e.left()

}

This ensures that no more exceptions are thrown. Instead, the method’s returning a Either.Left whenever the verification doesn’t succeed.
 

Using it as a client

The implementation is done. So how do we use this as a caller? We want to decide what to do based on whether the computation succeeded or not.

We can use a when expression thanks to having defined our Either as a sealed class.
 

val result = verifier.verify(jwt)

when (result) {

    is Either.Left -> ResponseEntity.badRequest().build()

    is Either.Right -> ResponseEntity.ok("Worked!")

}

This is equivalent to using the functional fold method. You provide a result for both possible values, thus folding it.
 

Operating on an Either value

I've just shown how to deal with an Either based on its two possible values (left and right). However, we want to also operate on the value throughout our application without having to unwrap and rewrap it each time, as that makes the code hard to read again.

We need to extend Either with two new methods, map and flatMap. Let's start with map:
 

fun <L, R, B> Either<L, R>.map(f: (R) -> B): Either<L, B> = when (this) {

    is Either.Left -> this.a.left()

    is Either.Right -> f(this.b).right()

}


We want to apply a function to our value contained inside the Either. Either is right biased, which means that once it becomes a Left value (i.e: an error), further computations won't be applied. Coming back to our ‘unsafeVerify method, we want to convert the result of that call, which we'll do thanks to our new map method:
 

verifier

    .unsafeVerify(jwt)

    .map { it.asToken() }


We’re still missing one case to cover. What if the operation we want to apply returns an Either itself? If we use map, we'll return an Either of an Either, nesting types until it's impossible to use anymore. To prevent that, we'll add a new method, flatMap.
 

fun <L, R, B> Either<L, R>.flatMap(f: (R) -> Either<L, B>): Either<L, B> = when (this) {

    is Either.Left -> this.a.left()

    is Either.Right -> f(this.b)

}


If you want to dig deeper into these functional concepts, check this article.

 

The arrow library

I provided a simple implementation of Either as an example. A better idea is to use an existing implementation. The excellent arrow library includes an Either type, among many other functional goodies.

One interesting feature of Arrow is that it provides its own flavor of the do notation, a way of making chained operations on types less cumbersome. In Arrow that is referred to as Monad comprehensions.

A normal chain of operations could look like this:
 

request.getHeader(Headers.AUTHORIZATION)

  .toEither()

  .flatMap { header ->

    header.extractToken()

      .flatMap { jwt ->

        verifier

          .verify(jwt)

          .map { token ->

            SecurityContextHolder.getContext().authentication = token

        }

      }

  }


We can get rid of the nested syntax, and make it look like this:
 

Either.fx {

    val (header) = request.getHeader(Headers.AUTHORIZATION).toEither()

    val (jwt) = header.extractToken()

    val (token) = verifier.verify(jwt)

    SecurityContextHolder.getContext().authentication = token

}


I find this easier to read, similar to the async/await syntax for JavaScript promises.
 

Appendix: A built-in solution

Since Kotlin 1.3, there has been a built-in way of dealing with computations that can fail. It is the Result class, which is typically used in a runCatching block:
 

runCatching {

    methodThatMightThrow()

}.getOrElse { ex ->

    dealWithTheException(ex)

}


This class can't be used as a return type yet, so we can't use it for our interface. Moreover, Either integrates nicely with all the other functionality provided by Arrow. In future articles, I plan to write in more detail about other things that you can do with it, such as converting it to an Option or chaining multiple calls in more complex examples.
 

Conclusion

Either is a great way to make the error handling in your code more explicit. Interfaces are clearer about what a call can actually return. Moreover, you can safely chain multiple operations, knowing that if something goes wrong along the way, the computation will short circuit. This is especially useful if you are running data pipelines as a batch operation. There you may not want to bail out on the first error, but rather run the batch in full while accumulating errors and successes.

Making the code more explicit reduces the amount of context that you need to keep in your head, which in turn makes the code easier to understand. Kotlin, combined with Arrow, works beautifully to enable this approach with a lightweight syntax that brings concepts from the functional world in a way that is easy and convenient to use.

Disclaimer: The statements and opinions expressed in this article are those of the author(s) and do not necessarily reflect the positions of Thoughtworks.

Keep up to date with our latest insights