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:
Firstly I’m using an implicit class to pimp EitherT, adding a recoverF method
The
F[_]
type parameter says ourF
should be a type constructor (wrapper type) e.g. Future/Monix Task/Option etc.A
represents the Left type andB
represents the Right typeThe 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 FutureWe pass an
op
parameter which just says given a Throwable, generate our Left type (A
)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