We’ll compare some code written with Either with similar code written using Exceptions and see how they are the same and how they differ.

In this example, we have 2 functionally identical pieces of code. The first part will throw Exceptions, the other uses Either.

f1 will return Int or Exception.

f2 will take Int and return Int or Exception.

combined will combine f1 and f2, returning Int or Exception.

handled will return a default value if any Exception occurs.

object ExceptionApp {
  object UseException {
    def f1: Int = throw new RuntimeException("exceptional problems")
    def f2(i: Int): Int = i + 1

    def combined: Int = {
      val a = f1
      f2(a)
    }

    def handled: Int = try {
      combined
    } catch {
      case t: Throwable => 0
    }
  }

  object UseEither {
    def f1: Either[Exception, Int] = Left(new RuntimeException("either problems"))
    def f2(i: Int): Either[Exception, Int] = Right(i + 1)

    def combined: Either[Exception, Int] = for {
      a <- f1
      r <- f2(a)
    } yield r

    def handled: Int = combined.fold(t => 0, identity)
  }
}

The big question is: "What have we gained?" To answer this, lets first look towards the advantages of Exception-throwing code.

It will be familiar to people without a Functional Programming background. They will have internalized that any result comes with an implicit or Exception.

You have to be very diligent to make sure you’re not throwing Exceptions. They can come from many places: libraries, math, IO. Even if you deal with all of these, you can still get an OutOfMemoryError. (Note that this kind of error can never be dealt with in a functional way, best you can do is a graceful shutdown)

While it’s easy to combine multiple Eithers using for, it becomes tricky when you combine them with List or Future. When you get a List[Future[Either[A]]], but you need a Future[Either[List[A]]].


Then the advantages!

We can make it clear that a function returns A, without Exceptions.

We can see which specific errors we might get back. See this example:

  object EitherWithSpecificExceptions {
    class GenerateError
    class AddError

    def f1: Either[GenerateError, Int] = Left(new GenerateError)
    def f2(i: Int): Either[AddError, Int] = Right(i + 1)

    def combined: Either[GenerateError | AddError, Int] = for {
      a <- f1
      r <- f2(a)
    } yield r

    def handled: Int = combined.fold(t => 0, identity)
  }

Remember Checked Exceptions? This gives all the advantages of those, but in a much more convenient way.

Something which used to be a language feature with special keywords and, can now be expressed inside the language (in code). Meaning we have a higher-order language. Either is a normal value, unlike an Exception. This has many benefits:

We could choose to cache the whole Either.

We can write our own version of Either.

shadow-left