In this post I’m exploring ZIO in ZIO course | Rock the JVM.
Error Basics #
Failures #
ZIOs can fail in multiple ways:
// Basic failure with custom error type
val aFailedZIO: IO[String, Nothing] =
ZIO.fail("Something went wrong")
// Failure with throwable
val failedWithThrowable: IO[RuntimeException, Nothing] =
ZIO.fail(new RuntimeException("Boom"))
// Transform error types using mapError
val failedWithDescription: ZIO[Any, String, Nothing] =
failedWithThrowable.mapError(_.getMessage)Unsafe Code #
What happens when exceptions are thrown inside ZIO.succeed
// Exceptions are not captured
// in the ZIO error channel
val badZIO: ZIO[Any, Nothing, Int] = ZIO.succeed {
println("Trying Something")
val string: String = null
string.length // This will throw, but ZIO doesn't expect it
}ZIO.attempt
#
When you’re not sure if code will throw exceptions, use ZIO.attempt:
// This properly captures exceptions in the error channel
// This uses Task, which is ZIO[Any, Throwable, Int]
val anAttempt: Task[Int] = ZIO.attempt {
println("Trying Something")
val string: String = null
string.length
}Error Recovery #
.catchAll()
#
Effectfully catch errors - the end result will also be an effect. catchAll recovers from error by transforming it into another effect.
Recovering from failures using different effect types:
// Recover with another effect that might fail
// Error type remains Throwable
val catchError: ZIO[Any, Throwable, Any] =
anAttempt.catchAll(e =>
ZIO.attempt(s"Returning a different value because $e")
)
// Recover with a guaranteed success
// Error type becomes Nothing
val catchErrorSuccess: ZIO[Any, Nothing, Any] =
anAttempt.catchAll(e =>
ZIO.succeed(s"Returning a different value because $e")
).catchSome()
#
Selectively handle only certain error types using pattern matching
// Handle only specific error types
// Error channel remains Throwable because it cannot be
// guaranteed at compile time that it will not fail
val catchSelectiveError: ZIO[Any, Throwable, Any] =
anAttempt.catchSome {
case e: RuntimeException =>
ZIO.succeed(s"Ignoring Runtime Exceptions: $e")
case _ =>
ZIO.succeed("Ignoring Everything else")
}
// Error type can be broadened when mixing different error types
// catchSome finds the lowest common denominator
// for all returned error types
val catchSelectiveError_v2: ZIO[Any, Serializable, Any] =
anAttempt.catchSome {
case e: RuntimeException =>
ZIO.succeed(s"Ignoring Runtime Exceptions: $e")
case _ =>
ZIO.fail("Ignoring Everything else")
}.orElse()
#
Providing a fallback ZIO when the first one fails:
// orElse completely overrides the error channel
val aBetterAttempt: ZIO[Any, Nothing, Int] =
anAttempt.orElse(ZIO.succeed(56))Result Handling #
.fold()
#
The fold signature takes two functions:
- one for failure
- one for success:
def fold[B](
failure: E => B,
success: A => B
)(implicit ev: CanFail[E], trace: Trace): URIO[R, B]val handleBoth: URIO[Any, String] = anAttempt.fold(
ex => s"Something bad happened: $ex",
// handle failed
value => s"length of the string was $value"
// handle success
).foldZIO()
#
Effectful fold: both success and failure handlers return ZIO effects.
The foldZIO signature allows both handlers to return effects:
def foldZIO[R1 <: R, E2, B](
failure: E => ZIO[R1, E2, B],
success: A => ZIO[R1, E2, B]
)(implicit ev: CanFail[E], trace: Trace): ZIO[R1, E2, B]val handleBoth_v2: ZIO[Any, Nothing, String] = anAttempt.foldZIO(
ex => ZIO.succeed(s"Something bad happened: $ex"),
// handle failed
value => ZIO.succeed(s"length of the string was $value")
// handle success
)Conversions #
Try → ZIO #
Converting Try to ZIO creates a Task with Throwable error type. The result type is ZIO[Any, Throwable, Int]:
val aTryToZIO: Task[Int] = ZIO.fromTry(Try(42 / 0))Either → ZIO #
Converting Either where Left becomes error and Right becomes success. The result is an IO[Int, String] which is ZIO[Any, Int, String]:
val anEither: Either[Int, String] = Right("Success!")
val anEitherToZIO: IO[Int, String] = ZIO.fromEither(anEither)ZIO → Either #
Transform ZIO into either success or failure in the value channel. The .either method produces a URIO[Any, Either[Throwable, Int]] which is ZIO[Any, Nothing, Either[Throwable, Int]]:
val eitherZIO: URIO[Any, Either[Throwable, Int]] = anAttempt.either
// Place Left of Either back into the Error Channel
val anAttempt_v2: ZIO[Any, Throwable, Int] = eitherZIO.absolveOption → ZIO #
Converting Option where None becomes a failure. The result type is IO[Option[Nothing], Int] which is ZIO[Any, Option[Nothing], Int]:
val anOption: IO[Option[Nothing], Int] = ZIO.fromOption(Some(42))Implementation Details #
Those conversions can be done manually using pattern matching:
// Custom Try to ZIO conversion
def try2ZIO[A](aTry: Try[A]): Task[A] = aTry match {
case Failure(exception) => ZIO.fail(exception)
case Success(value) => ZIO.succeed(value)
}
// Custom Either to ZIO conversion
def either2ZIO[A, B](anEither: Either[A, B]): IO[A, B] = anEither match {
case Left(value) => ZIO.fail(value)
case Right(value) => ZIO.succeed(value)
}// Custom ZIO to ZIO Either conversion using foldZIO
def zio2ZioEither[R, A, B](
zio: ZIO[R, A, B]
): ZIO[R, Nothing, Either[A, B]] =
zio.foldZIO(
error => ZIO.succeed(Left(error)),
value => ZIO.succeed(Right(value))
)
// Custom absolve implementation
def absolveZIO[R, A, B](
zio: ZIO[R, Nothing, Either[A, B]]
): ZIO[R, A, B] =
zio.flatMap {
case Left(e) => ZIO.fail(e)
case Right(v) => ZIO.succeed(v)
}// Custom Option to ZIO conversion
def option2ZIO[A](
anOption: Option[A]
): IO[Option[Nothing], A] = anOption match {
case Some(value) => ZIO.succeed(value)
case None => ZIO.fail(None)
}