Modelling shared error states for use in explicit error declarations via Eithers

Viewed 41

How can I manage error states that may be shared across multiple type hierarchies?

Consider an application that manages a database of people. It has a Person model and two methods in the API:

case class Person(name: String)

def createPerson(name: String): Either[CreateFailure, Person]
def renamePerson(newName: String): Either[RenameFailure, Person]

sealed trait CreateFailure
object CreateFailure {
  case object PersonAlreadyExists extends CreateFailure
  case object NamesCantHaveEmojis extends CreateFailure
}

sealed trait RenameFailure
object RenameFailure {
  case object CanOnlyRenameMinors extends RenameFailure
  case object NamesCantHaveEmojis extends RenameFailure
}

Each method can fail in its own unique way, but each method can also fail in the same way via NamesCantHaveEmojis. In the above trivial example I have two copies of this error state and this is triggering my code smell sensors. What if I need extend NamesCantHaveEmojis with new functionality, do I add it to both? Or have them each implement a common trait EmojiUtils that provides the shared functionality?

Scala 3's Union Types seem like they would fit the bill, but we haven't adopted Scala 3 yet.

type CreateFailure = PersonAlreadyExists | NamesCantHaveEmojis
type RenameFailure = CanOnlyRenameMinors | NamesCantHaveEmojis

It seems like many folks in the Scala community have been using Either for explicit error handling for a while now so I can only assume someone else has come across this scenario and found a good solution.

How can I share error states across multiple type hierarchies in Scala 2?

1 Answers

You can create NameCantHaveEmojis as object and extend two or more traits:

sealed trait CreateFailure
object CreateFailure {
  case object PersonAlreadyExists extends CreateFailure
}

sealed trait RenameFailure
object RenameFailure {
  case object CanOnlyRenameMinors extends RenameFailure
}

// you can extends here more than one trait using `with` keyword
case object NamesCantHaveEmojis extends CreateFailure with RenameFailure

and after you can use this error type in different match constructions:

val createError: CreateFailure = NamesCantHaveEmojis
createError match {
  case PersonAlreadyExists => println("PersonAlreadyExists")
  case NamesCantHaveEmojis => println("NamesCantHaveEmojis")
}
// NamesCantHaveEmojis printed

val renameError: RenameFailure = NamesCantHaveEmojis
renameError match {
  case CanOnlyRenameMinors => println("PersonAlreadyExists")
  case NamesCantHaveEmojis => println("NamesCantHaveEmojis") 
}
// NamesCantHaveEmojis printed

looks like createError and renameError are different failure types but they just one type and can be matched in different contexts.

Related