ThoughtWorks
  • Contato
  • Español
  • Deutsch
  • English
  • 中文
Visão geral
  • Cultura de engenharia, mentalidade de entrega

    Adote uma abordagem moderna ao desenvolvimento de software e entregue valor mais rapidamente

    Inteligência para tomada de decisões

    Explore seus dados para descobrir novas fontes de valor

  • Modelo de operação sem atritos

    Evolua a capacidade da sua organização de responder a mudanças

    Estratégia de plataforma

    Crie plataformas de tecnologia que se adaptam à sua estratégia de negócios

  • Design de experiência e inovação de produtos

    Planeje, execute e evolua rapidamente produtos e experiências excepcionais

    Parcerias

    Extraindo valor da nossa rede de parceiras para potencializar os resultados que entregamos a nossas clientes

Visão geral
  • Setor automotivo
  • Cleantech, energia e utilidade pública
  • Serviços financeiros e seguros
  • Saúde
  • Mídia
  • Organizações sem fins lucrativos
  • Setor público
  • Varejo e e-commerce
  • Viagem e transporte
Visão geral

Destaques

  • Tecnologia

    Uma análise abrangente de tecnologias e práticas de engenharia nas empresas

  • Negócios

    Mantenha-se em dia com as mais recentes tendências da indústria

  • Cultura

    Um espaço para conteúdo sobre desenvolvimento profissional e nossa visão sobre justiça social e inclusão

Ferramentas e Publicações Digitais

  • Technology Radar

    Um guia com opiniões firmes sobre as fronteiras da tecnologia

  • Perspectives

    Uma publicação para líderes digitais

  • Modelo de Fluência Digital

    Um modelo para priorizar as competências digitais necessárias para se navegar a incerteza

  • Decoder

    Um guia de A a Z sobre tecnologia para lideranças executivas

Todos os Insights

  • Artigos

    Visões de especialistas para ajudar seu negócio a crescer

  • Blogs

    Pontos de vista pessoais de ThoughtWorkers de todo o mundo

  • Livros

    Explore nossa vasta biblioteca

  • Podcasts

    Discussões instigantes sobre as últimas novidades em negócios e tecnologia

Visão geral
  • Processo de aplicação

    O que esperar de uma entrevista conosco

  • Pessoas em início ou mudança de carreira

    Comece sua jornada na tecnologia aqui

  • Vagas abertas

    Encontre oportunidades na sua região

  • Conecte-se

    Assine nossa newsletter mensal

Visão geral
  • Conferências e eventos
  • Diversidade e Inclusão
  • Notícias
  • Código aberto
  • Nossas lideranças
  • Transformação social
  • Español
  • Deutsch
  • English
  • 中文
ThoughtWorksMenu
  • Fechar   ✕
  • O que fazemos
  • Com quem trabalhamos
  • Insights
  • Carreiras
  • Sobre
  • Contato
  • Voltar
  • Fechar   ✕
  • Visão geral
  • Cultura de engenharia, mentalidade de entrega

    Adote uma abordagem moderna ao desenvolvimento de software e entregue valor mais rapidamente

  • Design de experiência e inovação de produtos

    Planeje, execute e evolua rapidamente produtos e experiências excepcionais

  • Modelo de operação sem atritos

    Evolua a capacidade da sua organização de responder a mudanças

  • Inteligência para tomada de decisões

    Explore seus dados para descobrir novas fontes de valor

  • Parcerias

    Extraindo valor da nossa rede de parceiras para potencializar os resultados que entregamos a nossas clientes

  • Estratégia de plataforma

    Crie plataformas de tecnologia que se adaptam à sua estratégia de negócios

  • Voltar
  • Fechar   ✕
  • Visão geral
  • Setor automotivo
  • Cleantech, energia e utilidade pública
  • Serviços financeiros e seguros
  • Saúde
  • Mídia
  • Organizações sem fins lucrativos
  • Setor público
  • Varejo e e-commerce
  • Viagem e transporte
  • Voltar
  • Fechar   ✕
  • Visão geral
  • Destaques

  • Tecnologia

    Uma análise abrangente de tecnologias e práticas de engenharia nas empresas

  • Negócios

    Mantenha-se em dia com as mais recentes tendências da indústria

  • Cultura

    Um espaço para conteúdo sobre desenvolvimento profissional e nossa visão sobre justiça social e inclusão

  • Ferramentas e Publicações Digitais

  • Technology Radar

    Um guia com opiniões firmes sobre as fronteiras da tecnologia

  • Perspectives

    Uma publicação para líderes digitais

  • Modelo de Fluência Digital

    Um modelo para priorizar as competências digitais necessárias para se navegar a incerteza

  • Decoder

    Um guia de A a Z sobre tecnologia para lideranças executivas

  • Todos os Insights

  • Artigos

    Visões de especialistas para ajudar seu negócio a crescer

  • Blogs

    Pontos de vista pessoais de ThoughtWorkers de todo o mundo

  • Livros

    Explore nossa vasta biblioteca

  • Podcasts

    Discussões instigantes sobre as últimas novidades em negócios e tecnologia

  • Voltar
  • Fechar   ✕
  • Visão geral
  • Processo de aplicação

    O que esperar de uma entrevista conosco

  • Pessoas em início ou mudança de carreira

    Comece sua jornada na tecnologia aqui

  • Vagas abertas

    Encontre oportunidades na sua região

  • Conecte-se

    Assine nossa newsletter mensal

  • Voltar
  • Fechar   ✕
  • Visão geral
  • Conferências e eventos
  • Diversidade e Inclusão
  • Notícias
  • Código aberto
  • Nossas lideranças
  • Transformação social
Blogs
Selecione um tema
Ver todos os tópicosFechar
Tecnologia 
Gestão de Projetos Agil Nuvem Entrega Contínua Ciência e Engenharia de Dados Defendendo a Internet Livre Arquitetura Evolutiva Design de Experiência IoT Linguagens, Ferramentas & Frameworks Modernização de sistemas legados Machine Learning & Artificial Intelligence Microsserviços Plataformas Segurança Testes de Software Estratégia de Tecnologia 
O negócio 
Serviços Financeiros Saúde Global Inovação Varejo Transformação 
Carreiras 
Dicas de Carreira Diversidade e Inclusão Transformação social 
Blogs

Topics

Escolha um tópico
  • Tecnologia
    Tecnologia
  • Tecnologia Visão Geral
  • Gestão de Projetos Agil
  • Nuvem
  • Entrega Contínua
  • Ciência e Engenharia de Dados
  • Defendendo a Internet Livre
  • Arquitetura Evolutiva
  • Design de Experiência
  • IoT
  • Linguagens, Ferramentas & Frameworks
  • Modernização de sistemas legados
  • Machine Learning & Artificial Intelligence
  • Microsserviços
  • Plataformas
  • Segurança
  • Testes de Software
  • Estratégia de Tecnologia
  • O negócio
    O negócio
  • O negócio Visão Geral
  • Serviços Financeiros
  • Saúde Global
  • Inovação
  • Varejo
  • Transformação
  • Carreiras
    Carreiras
  • Carreiras Visão Geral
  • Dicas de Carreira
  • Diversidade e Inclusão
  • Transformação social
Linguagens, Ferramentas & FrameworksTestes de SoftwareTecnologia

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
Posts relacionados
Ciência e Engenharia de Dados

Capturing data pipeline errors functionally with Writer Monads

Arun Manivannan
Saiba mais
Tecnologia

Lessons from inheriting another team’s codebase

Birgitta Böckeler
Saiba mais
Linguagens, Ferramentas & Frameworks

Como as linguagens de programação evoluíram

Rebecca Parsons
Saiba mais
  • O que fazemos
  • Com quem trabalhamos
  • Insights
  • Carreiras
  • Sobre
  • Contato

WeChat

×
QR code to ThoughtWorks China WeChat subscription account

Mídia e relações públicas | Política de privacidade | Modern Slavery statement ThoughtWorks| Acessibilidade | © 2021 ThoughtWorks, Inc.