Monad Error for the rest of us

Error handling has always been a problem not entirely solved in software development. Multiple approaches are used, from error codes to exceptions, all of them bringing along their own problems. In this post we aim to demonstrate a practical use of the Monad Error type class and how it can be used to develop generic error handling code.

All code snippets shown in this post are available on this repository.

Error handling in Scala

In order to propagate and handle application errors, Scala developers, especially those inclined to use functional programming, are accustomed to using monadic data types (Option, Try, Either, etc.) instead of exceptions or returning null.

Example

We are going to use, as an example, a function that takes a string as a parameter (which might fail to parse) and returns a parsed JSON object.

All imports will be further omitted to reduce the amount of code shown here.

def toJsonOpt(str: String): Option[Json] = {
    // (some code doing the actual parsing and will be further omitted in this post)
    if (success) Some(result) else None
}

def toJsonEither(str: String): Either[String, Json] =
    if (success) Right(result)
    else Left("Could not parse JSON String")

Compared to other types of error handling, this one brings multiple advantages:

  • avoiding Null Pointer Exceptions in the absence of a return value
  • avoiding Exception handling/declaration
  • explicit errors on the type system

Unfortunately, it also has some downsides:

  • usage of different error-handling datatypes by multiple libraries
  • error-handling datatypes must be specified when writing the code

This might become a problem when interacting with multiple libraries which use different error handling types or if you are developing a library and you want it to be as generic and compatible with existing code as possible.

The following code does not compile since the error-handling monad is different for both methods.

def readFile(): Try[String] = ???

def toJson(str: String): Either[Throwable, Json] = ???

for {
    fileContent <- readFile()
    parsedFile <- toJson(fileContent)
} yield parsedFile

Let’s say we want to implement the previous function toJsonand we want it to handle the error functionally while keeping the monad wrapping the error generic.

Pseudo-code of what we would like:

def toJson[F[_]](str: String): F[Json] =
    if (success) SUCCESS_CASE else ERROR_CASE

This allows users to specify the desired monad when calling the function.

How could we implement this?

Monad error to the rescue

MonadError is a type class that abstracts over error-handling monads, making it possible to raise or handle errors functionally while keeping the monad generic.

For the following examples we use cats‘ implementation of the MonadError type class.

def toJson[F[_]](str: String)(implicit M: MonadError[F, Throwable]): F[Json] =
    if (success) M.pure(result)
    else M.raiseError(ParseException("Could not parse JSON String"))

Now we can use the function with any type F[_] which has an instance of MonadError[F, Throwable]:

val parsedTry: Try[Json] = toJson[Try](content)

val parsedEither: Either[Throwable, Json] =
    toJson[Either[Throwable, ?]](content)

In case you are wondering what the ? in Either[Throwable, ?] is, check out the kind-projector compiler plugin for an explanation.

Useful methods

The MonadError type class implements some methods (besides the Monad ones) that might be useful for creating and transforming the wrapper monad.

  • pure and raiseError – create a value with a successful value or with an error, respectively.
  • fromEither / fromTry / fromOption / fromValidated – transforms a specific error monad into the specified MonadError
  • handleError / handleErrorWith / recover / recoverWith – the same semantic as Try‘s methods with the same name

Abstracting the error

Our code is more generic than before, but we still have to specify the error’s type when defining the function. Ideally, we would also like it if the function caller could specify its type (with some restrictions).

This can be accomplished by creating a type class to abstract an error that can be created from a set of arguments. It would also be possible to receive, as a parameter, a function that creates the error from those arguments, i.e. Throwable => E, E being the desired error type.

In the following example, we define a generic error that can be created from a String or a Throwable, let’s call it UIError.

trait UIError[A] {
  def errorFromString(str: String): A

  def errorFromThrowable(thr: Throwable): A
}

We can implement the function with the UIError:

def toJson[F[_], E](str: String)(implicit M: MonadError[F, E],
                                E: UIError[E]): F[Json] =
    if (success) M.pure(result)
    else M.raiseError(E.errorFromString("Could not parse JSON String"))

Now we can also specify the error’s type when calling the function, as long as there is an instance of UIError for this type (have a look at the repository for example implementations)

val parsedTry: Try[Json] = toJson[Try, Throwable](content)

val parsedEither: Either[String, Json] = toJson[Either[String, ?], String](content)

Wrapping up

We have seen monadic datatypes can be used as an alternative to raise and handle errors and the advantages they bring over conventional approaches. Unfortunately bringing some problems, when writing generic and compatible code is a priority. Using MonadError we are able to solve most of these issues while keeping the code fairly simple.

Don’t forget to check out the repository for more examples and to try it out for yourself.


Codacy is used by thousands of developers to analyze billions of lines of code every day!

Getting started is easy – and free! Just use your  GitHub, Bitbucket or Google account to sign up.

GET STARTED

Best PracticesSoftware Development

Related Articles