/ Cats

MonadError - Handling failed futures functionally

Scala's Either type allows us to deal with two paths of execution (Left or Right). Futures do the same (Success or Failure). Stacking Future and Either results in three execution paths (Success-Left, Success-Right, Failure) and it's easy to forget about the third scenario. Cats' MonadError allows us to reduce (Success-Left, Success-Right, Failure) to (Left or Right) by transforming a failed future into a Success-Left.

In my previous post I talked about Monad Transformers. I find EitherT[Future, MyError, ?] to be a particularly nice way of dealing with asynchronous operations:

def getUserId: Either[MyError, String] = ???
def getUser(userId: String): Future[Either[MyError, User]] = ???
def getAddress(user: User): Future[Either[MyError, Address]] = ???

val eitherT = for {
  userId <- EitherT.fromEither[Future](getUserId)
  user <- EitherT(getUser(userId))
  address <- EitherT(getAddress(user)
} yield address

Await.result(eitherT.value, 1.second)

So far so good, at the end of the flow we will either have a MyError or an Address. Well actually that's not quite true because the Futures themselves can fail. It's easy to forget about this edge case

Using recover / recoverWith

One option is to transform the futures themselves to ensure they never fail:

EitherT(getUser(userId).recover { case t => Left(MyError(t.getMessage) })

It works but it's a bit messy because we need to do this for every call that returns a Future i.e. getUser and getAddress


Alternatively we can handle this edge case using the Monad Transformer. Cats includes a Monad typeclass called MonadError for this scenario:

A monad that also allows you to raise and or handle an error value. This type class allows one to abstract over error-handling monads

So how do we use it. We use MonadError's recoverWith method to transform our original EitherT into a version that recovers from the failed future:

val eitherT = for { ... }

val recoveredEitherT = MonadError[EitherT[Future, MyError, Address], Throwable].recoverWith(eitherT) {
  case t => EitherT.leftT[Future, Address](MyError(t.getMessage))


We can make this a bit more generic, allowing us to handle any EitherT stack:

implicit class RecoveringEitherT[F[_], A, B](underlying: EitherT[F, A, B])(implicit me: MonadError[F, Throwable]) {
  def recoverF(op: Throwable => A) = MonadError[EitherT[F, A, ?], Throwable].recoverWith(underlying) {
    case t => EitherT.fromEither[F](op(t).asLeft[B])

val eitherT = ???
val recoveredEitherT = eitherT.recoverF(t => MyError(t.getMessage))

Let's break this down:

  1. Firstly I'm using an implicit class to pimp EitherT, adding a recoverF method

  2. The F[_] type parameter says our F should be a type constructor (wrapper type) e.g. Future/Monix Task/Option etc. A represents the Left type and B represents the Right type

  3. The implicit MonadError[F, Throwable] is the interesting parameter. It tells the compiler we need a MonadError instance for our F and Throwable. Cats includes such an implementation for Future

  4. We pass an op parameter which just says given a Throwable, generate our Left type (A)

  5. Finally we use MonadError's recoverWith to transform Throwable to A using the provided op

Why use MonadError

As I mentioned before, we can simply recover from any future before wrapping it in an EitherT. Alternatively we can recover a failed future in the final step when we turn the EitherT back into a future:

val eventualResult = eitherT.value.recover { case t => Left(MyError(t.getMessage)) }

Both approaches are valid but they're either too fine or coarse grained. We probably don't want to write code to recover from every future. Equally a "catch all" style handler is probably not very useful