Handle
Handle[F, E]
extends Raise
with the ability to also handle raised errors.
It adds the handleWith
function, which can be used to recover from errors and therefore allow continuing computations.
The handleWith
function has the following signature:
def handleWith[A](fa: F[A])(f: E => F[A]): F[A]
This function is fully equivalent to handleErrorWith
from cats.ApplicativeError
and in general Handle
is fully equivalent to cats.ApplicativeError
,
but is not a subtype of Applicative
and therefore doesn't cause any ambiguitites.
Let's look at an example of how to use this function:
import cats._
import cats.syntax.all._
import cats.mtl._
import cats.mtl.implicits._
def parseNumber[F[_]: Applicative](in: String)(implicit F: Raise[F, String]): F[Int] = {
// this function might raise an error
if (in.matches("-?[0-9]+")) in.toInt.pure[F]
else F.raise(show"'$in' could not be parsed as a number")
}
def notRecovered[F[_]: Applicative](implicit F: Raise[F, String]): F[Boolean] = {
parseNumber[F]("foo")
.map(n => if (n > 5) true else false)
}
def recovered[F[_]: Applicative](implicit F: Handle[F, String]): F[Boolean] = {
parseNumber[F]("foo")
.handle[String](_ => 0) // Recover from error with fallback value
.map(n => if (n > 5) true else false)
}
val err = notRecovered[Either[String, *]]
// err: Either[String, Boolean] = Left(
// value = "'foo' could not be parsed as a number"
// )
val result = recovered[Either[String, *]]
// result: Either[String, Boolean] = Right(value = false)
Error propagation with allow
and rescue
In addition to the traditional raise
and handleWith
mechanism, Handle
supports allow
and rescue
functions which provides a concise, intuitive and performant way to raise and handle domain-specific errors, with syntax inspired by try/catch
blocks.
import cats.*
import cats.syntax.all.*
import cats.mtl.*
import cats.mtl.Handle.*
type F[A] = Either[Throwable, A]
enum DomainError:
case Failed
case Derped
def foo(using h: Raise[F, DomainError]): F[String] = Either.right("foo")
def bar(using h: Handle[F, DomainError]): F[String] = h.raise(DomainError.Failed)
val submarine: F[String] =
allow:
foo *> bar
.rescue:
case DomainError.Failed => Either.right("Handled Failed")
case DomainError.Derped => Either.right("Handled Derped")
// submarine: Either[Throwable, String] = Right(value = "Handled Failed")
import cats._
import cats.syntax.all._
import cats.mtl._
import cats.mtl.Handle._
type F[A] = Either[Throwable, A]
sealed trait DomainError
object DomainError {
case object Failed extends DomainError
case object Derped extends DomainError
}
def foo(implicit h: Raise[F, DomainError]): F[String] = Either.right("foo")
def bar(implicit h: Handle[F, DomainError]): F[String] = h.raise(DomainError.Failed)
val submarine: F[String] =
allowF[F, DomainError] { implicit h =>
foo *> bar
} rescue {
case DomainError.Failed => Either.right("Handled Failed")
case DomainError.Derped => Either.right("Handled Derped")
}
// submarine: Either[Throwable, String] = Right(value = "Handled Failed")
Notice that DomainError
is a regular ADT (algebraic data type) and does not extend Exception
. With allow
(allowF
in Scala 2) and rescue
, you can raise and handle errors of your own types—not just exceptions—using syntax that feels like familiar exception handling, but in a purely functional and type-safe way.
This pattern makes error handling more readable and expressive, and often removes the need for transformers like EitherT
or IorT
in regular code.