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?