Replace Exceptions with Sum Types

Summary

You have a function that either returns a value or throws an exception.

Change the function's return type to a disjunction of the original return type and the exception.

We use the following form for our disjunction, called Either:

sealed trait Either[A,B]
case class Left[A,B](x: A) extends Either[A,B]
case class Right[A,B](x: B) extends Either[A,B]

This is similar to Scala's built-in Either type, but with support for for-comprehensions.

Motivation

Checked exceptions force the developer to wrap their code in try/catch blocks, yielding noisy code that's hard to reason about, hard to combine, and impure in languages where try/catch blocks are not expressions.

Unchecked exceptions allow the developer to forget to wrap their code in try/catch blocks, creating a runtime timebomb.

Mechanics

When encountering an expression that throws an exception, note the types of the exception that it can throw, and the value that it can return. We'll call these A and B, respectively.

Remodel the expression's return type to Either[A,B].

Change any throw e into Left(e).

Change any return b into Right(b).

Example

Exceptions

def div(x: Int, y: Int): Int = x / y
val quotient: Int =
  try {
    div(42, 0)
  } catch {
    case e: ArithmeticException => -999
  }

println(s"quotient: ${quotient}") // quotient: -999

Either

def divE(x: Int, y: Int): Either[Exception, Int] =
  try {
    Right(x / y)
  } catch {
    case e: Exception => Left(e)
  }
val quotientE: Either[Exception,Int] = divE(42, 0)
println(s"quotientE: ${quotientE}") // quotientE: Left(java.lang.ArithmeticException: / by zero)

Right-biased either

implicit class RightBiasedEither[A,B](e: Either[A,B]) {

  def flatMap[C](f: B => Either[A,C]): Either[A,C] =
    e match {
      case Left(a) => Left(a)
      case Right(b) => f(b)
    }

  def map[C](f: B => C): Either[A,C] =
    flatMap { b =>
      Right(f(b))
    }

}
val quotientE2: Either[Exception,Int] =
  for {
    a <- divE(42, 7)
    b <- divE(a, 0)
  } yield b

println(s"quotientE2: ${quotientE2}") // quotientE2: Left(java.lang.ArithmeticException: / by zero)

Demo

quotient: -999
quotientE: Left(java.lang.ArithmeticException: / by zero)
quotientE2: Left(java.lang.ArithmeticException: / by zero)