Skip to content
AgileLogics
6 min read

Either and Validation

Prerequisites: Error handling: exceptions and Try

Either[L, R] holds one of two values: Left(error) or Right(value). By convention, Right is the success ("right" as in correct). Unlike Try, the error can be any type you choose.

enum SignupError:
  case EmptyName
  case InvalidEmail(email: String)
  case TooYoung(age: Int)

case class Signup(name: String, email: String, age: Int)

def validateName(name: String): Either[SignupError, String] =
  if name.trim.nonEmpty then Right(name.trim) else Left(SignupError.EmptyName)

def validateEmail(email: String): Either[SignupError, String] =
  if email.contains("@") then Right(email) else Left(SignupError.InvalidEmail(email))

def validateAge(age: Int): Either[SignupError, Int] =
  if age >= 16 then Right(age) else Left(SignupError.TooYoung(age))

def signup(name: String, email: String, age: Int): Either[SignupError, Signup] =
  for
    n <- validateName(name)
    e <- validateEmail(email)
    a <- validateAge(age)
  yield Signup(n, e, a)

@main def validation(): Unit =
  println(signup("Ada", "ada@example.com", 36))
  println(signup("", "ada@example.com", 36))
  println(signup("Sam", "sam.example.com", 12))
Right(Signup(Ada,ada@example.com,36))
Left(EmptyName)
Left(InvalidEmail(sam.example.com))
  • The error type is an enum, so callers can match on exactly what went wrong — and the compiler checks they handle every case.
  • The for-comprehension stops at the first Left ("fail fast"). The third signup had two problems, but only the email was reported.

Working with Either

@main def eitherOps(): Unit =
  val ok: Either[String, Int] = Right(20)
  val err: Either[String, Int] = Left("not a number")

  println(ok.map(_ + 1))
  println(err.map(_ + 1))
  println(err.getOrElse(0))
  println(ok.fold(e => s"error: $e", v => s"value: $v"))
  println(err.left.map(_.toUpperCase))
  println("7".toIntOption.toRight("not a number"))
  println(Right(5).filterOrElse(_ > 10, "too small"))
Right(21)
Left(not a number)
0
value: 20
Left(NOT A NUMBER)
Right(7)
Left(too small)

Collecting all errors

To report every problem (as forms should), validate each field separately and combine the results:

def check(field: String, ok: Boolean): Either[String, Unit] =
  if ok then Right(()) else Left(s"$field is invalid")

@main def allErrors(): Unit =
  val checks = List(
    check("name", "".nonEmpty),
    check("email", "sam.example.com".contains("@")),
    check("age", 12 >= 16)
  )
  val errors = checks.collect { case Left(e) => e }
  if errors.isEmpty then println("valid") else println(errors.mkString("; "))
name is invalid; email is invalid; age is invalid

Libraries like Cats provide Validated / ValidatedNec, which accumulate errors while combining valid results into your case class.