Master
ThoughtWorks
Menu
Close
  • What we do
    • Go to overview
    • Customer Experience, Product and Design
    • Data Strategy, Engineering and Analytics
    • Digital Transformation and Operations
    • Enterprise Modernization, Platforms and Cloud
  • Who we work with
    • Go to overview
    • Automotive
    • Healthcare
    • Public Sector
    • Cleantech, Energy and Utilities
    • Media and Publishing
    • Retail and E-commerce
    • Financial Services and Insurance
    • Not-for-profit
    • Travel and Transport
  • Insights
    • Go to overview
    • Featured

      • Technology

        An in-depth exploration of enterprise technology and engineering excellence

      • Business

        Keep up to date with the latest business and industry insights for digital leaders

      • Culture

        The place for career-building content and tips, and our view on social justice and inclusivity

    • Digital Publications and Tools

      • Technology Radar

        An opinionated guide to technology frontiers

      • Perspectives

        A publication for digital leaders

      • Digital Fluency Model

        A model for prioritizing the digital capabilities needed to navigate uncertainty

      • Decoder

        The business execs' A-Z guide to technology

    • All Insights

      • Articles

        Expert insights to help your business grow

      • Blogs

        Personal perspectives from ThoughtWorkers around the globe

      • Books

        Explore our extensive library

      • Podcasts

        Captivating conversations on the latest in business and tech

  • Careers
    • Go to overview
    • Application process

      What to expect as you interview with us

    • Grads and career changers

      Start your tech career on the right foot

    • Search jobs

      Find open positions in your region

    • Stay connected

      Sign up for our monthly newsletter

  • About
    • Go to overview
    • Our Purpose
    • Awards and Recognition
    • Diversity and Inclusion
    • Our Leaders
    • Partnerships
    • News
    • Conferences and Events
  • Contact
Global | English
  • United States United States
    English
  • China China
    中文 | English
  • India India
    English
  • Canada Canada
    English
  • Singapore Singapore
    English
  • United Kingdom United Kingdom
    English
  • Australia Australia
    English
  • Germany Germany
    English | Deutsch
  • Brazil Brazil
    English | Português
  • Spain Spain
    English | Español
  • Global Global
    English
Blogs
Select a topic
View all topicsClose
Technology 
Agile Project Management Cloud Continuous Delivery  Data Science & Engineering Defending the Free Internet Evolutionary Architecture Experience Design IoT Languages, Tools & Frameworks Legacy Modernization Machine Learning & Artificial Intelligence Microservices Platforms Security Software Testing Technology Strategy 
Business 
Financial Services Global Health Innovation Retail  Transformation 
Careers 
Career Hacks Diversity & Inclusion Social Change 
Blogs

Topics

Choose a topic
  • Technology
    Technology
  • Technology Overview
  • Agile Project Management
  • Cloud
  • Continuous Delivery
  • Data Science & Engineering
  • Defending the Free Internet
  • Evolutionary Architecture
  • Experience Design
  • IoT
  • Languages, Tools & Frameworks
  • Legacy Modernization
  • Machine Learning & Artificial Intelligence
  • Microservices
  • Platforms
  • Security
  • Software Testing
  • Technology Strategy
  • Business
    Business
  • Business Overview
  • Financial Services
  • Global Health
  • Innovation
  • Retail
  • Transformation
  • Careers
    Careers
  • Careers Overview
  • Career Hacks
  • Diversity & Inclusion
  • Social Change
Languages, Tools & FrameworksSoftware TestingTechnology

The Either data type as an alternative to throwing exceptions

Mario Fernandez Mario Fernandez

Published: Apr 22, 2020

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.

Technology Hub

An in-depth exploration of enterprise technology and engineering excellence.

Explore
Related blogs
Data Science & Engineering

Capturing data pipeline errors functionally with Writer Monads

Arun Manivannan
Learn more
Technology

Lessons from inheriting another team’s codebase

Birgitta Böckeler
Learn more
Languages, Tools & Frameworks

How programming languages have evolved

Rebecca Parsons
Learn more
Master
Privacy policy | Modern Slavery statement | Accessibility
Connect with us
×

WeChat

QR code to ThoughtWorks China WeChat subscription account
© 2021 ThoughtWorks, Inc.