Introduction

In Scala, it’s a common practice to handle errors or perform validation using Option or Either. For example, a form on a website may be validated on a server by using a series of Eithers, which will return the valid model data or a message explaining the problems in the form submission. However, adding more complicated concerns into the Either sequence, such as Options or Futures, requires a lot of boilerplate.

To see how this might happen, let’s consider a simple example, account registration. In our case, an account consists of a username, a password and an email address.

case class Account(username: String, password: String, email: String)

To register for an account, a new user has to provide all three of these fields, which usually follow a set of restrictions. For example, most websites require passwords to have a minimum length, and some websites don’t allow usernames to have spaces. The user registration form might be validated like this:

// Note: This example requires Scala 2.12, which allows Either to be used
// in for comprehensions

import scala.util.{ Either, Left, Right }

type ErrorOr[A] = Either[String, A]

def validateUsername(username: String): ErrorOr[String] = ???
def validatePassword(password: String): ErrorOr[String] = ???
def validateEmail(email: String): ErrorOr[String] = ???

def validateAccount(usernameInput: String, passwordInput: String, emailInput: String): ErrorOr[Account] =
  for {
    username <- validateUsername(usernameInput)
    password <- validatePassword(passwordInput)
    email <- validateEmail(emailInput)
  } yield Account(usernameInput, passwordInput, email)

This looks clean, but creating the actual account isn’t. For example, user registration might involve checking for existing accounts, saving the account a database, and sending a welcome email. In addition, the aforementioned actions may need to occur asynchronously, which means that we have to handle Futures, as well.

def findAccountWithEmail(email: String): Future[Option[Account]] = ???
def sendWelcomeEmail(email: String): Future[Unit] = ???
def insertAccountIntoDatabase(newAccount: Account): Future[Account] = ???

def registerAccount(usernameInput: String, passwordInput: String, emailInput: String): Future[ErrorOr[Account]] = {
  validateAccount(usernameInput, passwordInput, emailInput).fold(
    error => Future.successful(Left(error)),
    validAccount => findAccountWithEmail(emailInput) flatMap {
      case Some(_) =>
        val errorMessage = "Account with this email already exists!"
        Future.successful(Left(errorMessage))
      case None =>
        for {
          _ <- insertAccountIntoDatabase(validAccount)
          _ <- sendWelcomeEmail(validAccount.email)
        } yield Right(validAccount)
    }
  )
}

There’s a lot of noise that distracts from how the process works. Also, it’s a pain to write the boilerplate, especially since form validation is quite common.

The problem is that the steps don’t compose very well. It would be far nicer if we could just write a single for-comprehension that deals strictly with Either and let some other underlying mechanism handle the Future boilerplate. For example, a clean hypothetical example of a registerAccount function would be

def registerAccount(usernameInput: String, passwordInput: String, emailInput: String): Future[ErrorOr[Account]] =
  for {
    validAccount <- validateAccount(usernameInput, passwordInput, emailInput)
    accountOpt <- findAccount(validAccount.email) if accountOpt.isEmpty
    _ <- insertAccountIntoDatabase(validAccount)
    _ <- sendWelcomeEmail(validAccount.email)
  } yield validAccount

Abstracting Away Either Handling

We can get pretty close to that hypothetical example by using the EitherT monad transformer. It’s not necessary to know what a “monad transformer” is, only that EitherT is a wrapper for some effectful type (e.g. Option or Future) that can abstract away the effect and handle the contents of the type in a more convenient manner. I’m going to use the EitherT from cats, but the EitherT from scalaz should also work (albeit with different function names).

Using EitherT is pretty straightforward: wrap your desired data in EitherT, compose the EitherT values using a for-comprehension, and then extract the final wrapped F[Either[B, A]] using the value method, where F is the effectful type, B is the error type, and A is the type of the valid data. Here’s an example using Future:

type Result[A] = EitherT[Future, String, A] // wraps a Future[Either[String, A]]

val numberET: Result[Int] = EitherT.pure(5) // pure has type A  => EitherT[F, B, A]
val numberOpt = Some(10)

val finalEitherT = for {
  n <- numberET
  // fromOption transforms an Option into an Right if it exists, or a Left with
  // erroraneous value otherwise.
  numberOpt <- EitherT.fromOption(numberOpt, "Number not defined")
} yield (n + numberOpt)

val myFuture: Future[Either[String, Int]] = finalEitherT.value // convert EitherT to Future

val lifted: Result[Int] = EitherT.fromEither(Right(5)) // convert Either to EitherT

Failures work as expected, conforming to the short-circuiting nature of Either:

val successful: Result[Int] = EitherT.pure(5)
val fail: Result[Int] = EitherT.fromEither(Left("Nope"))
val neverReached: Result[Int] = EitherT.pure(5)

val myEitherT: Result[Int] = for {
  a <- successful
  b <- fail
  c <- neverReached
} yield c

println(myEitherT.value) // Nope

Try out the EitherT functions with different values and combinations, and see what you get!

There’s also a convenient function called cond, which is similar to an if-statement for EitherT.

def asyncDivide(n: Int, divisor: Int): Result[Int] =
  EitherT.cond(divisor != 0, n / divisor, "Cannot divide by zero")

asyncDivide(5, 0) // Cannot divide by zero
asyncDivide(10, 2) // Successful

Reimplementing registerAccount

Now that we’re armed with EitherT, let’s reimplement registerAccount in a more elegant way. The goal is to make the logic more explicit by ordering each step sequentially. First, let’s bring back the handy Result alias:

type Result[A] = EitherT[Future, String, A]

Next, let’s refactor the validateAccount logic. Since Either is already returned for each step, all we have to do is lift each Either with EitherT.fromEither.

def validateAccount(usernameInput: String, passwordInput: String, emailInput: String): Result[Account] =
  for {
    username <- EitherT.fromEither(validateUsername(usernameInput))
    password <- EitherT.fromEither(validatePassword(passwordInput))
    email <- EitherT.fromEither(validateEmail(emailInput))
  } yield Account(usernameInput, passwordInput, email)

The problematic part is testing for the existing account, since it causes the logic to branch off:

def findAccountWithEmail(email: String): Future[Option[Account]] = ???

If the account already exists, it should return an error message and stop the registration immediately. Otherwise, it should continue with registration.

def testForExistingAccount(email: String): Result[Unit] =
    EitherT(findAccountWithEmail(email) map {
      case Some(_) => Left("An account with this email already exists")
      case None => Right(())
    })

Now, all that remains is to compose the steps in the registerAccount function. This should be trivial, since the types that we’re dealing with are Future, Either, and EitherT, which can all be combined into EitherT in a single for-comprehension.

def registerAccount(usernameInput: String, passwordInput: String, emailInput: String): Future[ErrorOr[Account]] = {
  val eitherT: Result[Account] = for {
    newAccount <- validateAccount(usernameInput, passwordInput, emailInput)
    _ <- testForExistingAccount(emailInput)
    _ <- EitherT(insertAccountIntoDatabase(newAccount))
    _ <- EitherT(sendWelcomeEmail(newAccount.email))
  } yield newAccount

  eitherT.value
}

This is pretty close to the ideal code, and it’s very easy to understand!

Conclusion

I’ve shown that using EitherT can make error handling far more readable. Like I briefly mentioned above, EitherT works for effectful types such as Option[Either[String, Int]] or IO[Either[String, Int]]. Since these types can be quite general, it’s easy to see that EitherT has a large variety of use cases, especially for short-circuiting steps. To find more examples on how to use EitherT, consult the EitherT docs on the Cats website.

Sometimes, it might be desirable to use EitherT in situations involving parallel validation (e.g. validate all fields at the same time and return a list of all errors). In that case, with some effort, Validated (or Validation) can be used with EitherT to add parallel validation. Use Either for sequential validation and Validated for parallel validation for the best effects! See the Either and Validation documentation for more information.

Update: Thanks to @Deliganli for fixing a mistake in the Reimplementing registerAccount section. I also fixed another mistake and added a reference to the EitherT docs on the Cats website.