Toby Hobson

MonadError - Handling failed futures functionally

banner.png
MonadError - Handling failed futures functionally
Estimated reading time: 4 minutes

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.

You can find complete examples of the concepts discussed on my blog in my Github repo . In particular, check out the monad error examples

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[UserNotFound, Int] = ???
def fetchUser(id: Int): Future[Either[UserNotFound, User]] = ???
def fetchOrder(user: User): Future[Either[OrderNotFound, Order]] = ???

val eitherT = for {
  userId <- EitherT.fromEither[Future](getUserId: Either[ServiceError, Int])
  user <- EitherT(fetchUser(userId): Future[Either[ServiceError, User]])
  order <- EitherT(fetchOrder(user): Future[Either[ServiceError, Order]])
} yield order

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

So far so good, at the end of the flow we will either have a ServiceError or an Order. 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(fetchUser(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. fetchUser and fetchAddress

MonadError

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, ?], Throwable].recoverWith(eitherT) {
  case t => EitherT.leftT[Future, Address](MyError(t.getMessage))
}

What’s the ? you may ask? As usual it’s a compiler hack. Lets take a look at the signature for MonadError.apply: def apply[F[_], E] so it expects an F[_] (the underlying Monad) and an E (the error/failure type - in our case Throwable). If you’ve been following my other posts you’ll recognise that we need F[_] but our EitherT is actually F[G[_], A, B]. As usual we fix two of the three types and leave one free to give us F[_]. I’m using the kind compiler plugin to reduce boilerplate by using an anonymous type. I could have achieved the same result using an explicit type:

type MyEitherT[A] = EitherT[Future, ServiceError, A]

val recoveredEitherT = MonadError[MyEitherT, Throwable].recoverWith(eitherT) {
  case t => EitherT.leftT[Future, Order](OtherError(t.getMessage): ServiceError)
}

Reuse

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 recoveredEitherT2 = eitherT.recoverF(t => OtherError(s"future failed - ${t.getMessage}"): ServiceError)

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

comments powered by Disqus

Need help with your project?

Do you need some help or guidance with your project? Reach out to me (email is best)