🎓 A1Learner
🔍
0 of 48 lessons completed

Scala Error Design

Error design is the choice of how a program reports each kind of failure. A good design gives each failure kind its own named type.

A sealed trait gives each error kind its own case

NotFound carries the missing id. Invalid carries the reason. The match turns each case into a message.

sealed trait AppError
case class NotFound(id: Int) extends AppError
case class Invalid(reason: String) extends AppError
def message(e: AppError): String =
  e match
    case NotFound(id) => "missing " + id
    case Invalid(reason) => "invalid: " + reason
@main def main(): Unit =
  println(message(NotFound(9)))
  println(message(Invalid("age is negative")))

Either returns the error kind or the good value

A known id gives the name on the right. An unknown id gives the NotFound error on the left.

sealed trait AppError
case class NotFound(id: Int) extends AppError
case class Invalid(reason: String) extends AppError
def find(id: Int): Either[AppError, String] =
  if id == 7 then Right("Ana") else Left(NotFound(id))
@main def main(): Unit =
  println(find(7))
  println(find(9))

An exhaustive match needs no catch-all

The match covers both error kinds. Each kind maps to its own status code.

sealed trait AppError
case class NotFound(id: Int) extends AppError
case class Invalid(reason: String) extends AppError
def code(e: AppError): Int =
  e match
    case NotFound(_) => 404
    case Invalid(_) => 400
@main def main(): Unit =
  println(code(NotFound(3)))
  println(code(Invalid("bad")))

Example

sealed trait AppError
case class NotFound(id: Int) extends AppError
case class Invalid(reason: String) extends AppError
def find(id: Int): Either[AppError, String] =
  if id < 0 then Left(Invalid("id is negative"))
  else if id == 7 then Right("Ana")
  else Left(NotFound(id))
def report(result: Either[AppError, String]): String =
  result match
    case Right(name) => "found " + name
    case Left(NotFound(id)) => "missing " + id
    case Left(Invalid(reason)) => "invalid: " + reason
@main def main(): Unit =
  println(report(find(7)))
  println(report(find(9)))
  println(report(find(-1)))

Key points

  • Sealed trait: a sealed trait lists every error kind in one hierarchy.
  • One case per kind: each failure kind gets its own case class with its own details.
  • Exhaustive match: a match over the sealed trait covers every case and needs no catch-all.
  • Error value: returning an error as a value keeps the failure visible to the caller.
  • Detail in the case: the case class carries the detail, like the missing id.

In this example, find returns one of three results for each id. A known id gives a name, an unknown id gives NotFound, and a negative id gives Invalid. The report match turns each result into its own plain message.

Note

a thrown exception jumps to a catch case, while a returned error travels as an ordinary value.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Scastie

Exercise

Why use a sealed trait for errors?