(Based on this, strongly recommended for an in-depth article, tutorial on error dealing with Cats)
Business/application errors should be modeled in the application and be part of a monad transformer (usually EitherT
)to enrich the effect with the errors or the value depending on the result of a computation (for a longer explanation, example, see here).
But this only counts for those business or application errors (those ones you can control as they are part of the business logic), there are technical errors due to connection failures, file system issues, misconfiguration, ... Those ones are real exceptional errors which have to be dealt with. Those exception errors (actually exceptions) can be raised and recovered inside IO
s
and have a ubiquitous use across many libs.
They are handled once and typically in the upper levels of the application, mostly using the methods described just below. There are more, and for a quite complete and exhaustive list and reference (with snippets), can be worth to have a look here.
Just a quick simple example:
def allFoos: IO[Map[Int, String]] = IO(Map(1->"one", 2->"two", 3->"thr"))
def pickOne(id: Int): IO[String] = allFoos.flatMap(m => m.get(id).fold( IO.raiseError[String](new Exception("Not found in map")) )(s => s.pure[IO]))
def safePick(id: Int): IO[String] = pickOne(2).recover {
case e: Throwable => "Nothing found"
}
scala> safePick(1).unsafeRunSync()
val res43: String = one
scala> safePick(12).unsafeRunSync()
val res44: String = Nothing found
scala> pickOne(12).unsafeRunSync()
java.lang.Exception
at $anonfun$pickOne$2(<console>:1)
at scala.Option.fold(Option.scala:263)
at $anonfun$pickOne$1(<console>:1)
at apply @ allFoos(<console>:1)
at flatMap @ pickOne(<console>:1)
So, recover
accepts as parameter a PartialFunction[Throwable, String]
and returns IO[String]
(F[A]
generically).
As it's seen, the value returned by the partial function has a simple type (not wrapped in any effect)
Just the counterpart of the previous one, where the value returned by the partial function as to be wrapped in the effect.
So, we just had to implement:
def safePickWith(id: Int): IO[String] = pickOne(id).recoverWith {
case e: Throwable => IO("NNothing foundd")
}
scala> safePickWith(3).unsafeRunSync()
val res45: String = thr
scala> safePickWith(4).unsafeRunSync()
val res46: String = NNothing foundd
handleError[B >: A](f: (Throwable) => B): IO[B]
handleErrorWith[B >: A](f: (Throwable) => IO[B]): IO[B]
These ones just map the error (instance of Throwable
) into a value which is a subtype (B >: A
) of the value
wrapped by the effect. The latter define the function f to return the value wrapped in the effect.
The main difference between these error handler*
methods and the recover*
ones is the latter needs a partial function,
while the former just a lambda:
def handlePick(id: Int) = pickOne(id).handleError {
e: Throwable => s"Handling not found: ${e.getMessage}"
}
scala> handlePick(11).unsafeRunSync()
val res60: String = Handling not found: Not found in map
Civilized error handling, in the sense it materializes the result of the computation into an Either
value (wrapped
in the effect) such that we always have a value as the result of the computation and we can do stuff with it. On a
successful computation we will get a Right
, otherwise a Left
with the error (exception)