Skip to content

Instantly share code, notes, and snippets.

@telekosmos
Created April 1, 2022 09:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save telekosmos/dd2ec4943d72989e4f5212fcc0c6429a to your computer and use it in GitHub Desktop.
Save telekosmos/dd2ec4943d72989e4f5212fcc0c6429a to your computer and use it in GitHub Desktop.
Short quick description on exception handling in an effectful cats application

Error dealing and handling

(Based on this, strongly recommended for an in-depth article, tutorial on error dealing with Cats)

Business errors vs technical errors

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 IOs 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.

Error dealing methods

recover

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)

recoverWith

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*

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

attempt

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment