Handling Errors
This section looks at some of the common ways to detect and respond to failure.
Either
You can surface failures with ZIO#either
, which takes an ZIO[R, E, A]
and produces an ZIO[R, Nothing, Either[E, A]]
.
val zeither: UIO[Either[String, Int]] =
IO.fail("Uh oh!").either
You can submerge failures with ZIO.absolve
, which is the opposite of either
and turns an ZIO[R, Nothing, Either[E, A]]
into a ZIO[R, E, A]
:
def sqrt(io: UIO[Double]): IO[String, Double] =
ZIO.absolve(
io.map(value =>
if (value < 0.0) Left("Value must be >= 0.0")
else Right(Math.sqrt(value))
)
)
Catching All Errors
If you want to catch and recover from all types of errors and effectfully attempt recovery, you can use the catchAll
method:
val z: IO[IOException, Array[Byte]] =
openFile("primary.json").catchAll(_ =>
openFile("backup.json"))
In the callback passed to catchAll
, you may return an effect with a different error type (or perhaps Nothing
), which will be reflected in the type of effect returned by catchAll
.
Catching Some Errors
If you want to catch and recover from only some types of exceptions and effectfully attempt recovery, you can use the catchSome
method:
val data: IO[IOException, Array[Byte]] =
openFile("primary.data").catchSome {
case _ : FileNotFoundException =>
openFile("backup.data")
}
Unlike catchAll
, catchSome
cannot reduce or eliminate the error type, although it can widen the error type to a broader class of errors.
Fallback
You can try one effect, or, if it fails, try another effect, with the orElse
combinator:
val primaryOrBackupData: IO[IOException, Array[Byte]] =
openFile("primary.data").orElse(openFile("backup.data"))
Folding
Scala's Option
and Either
data types have fold
, which let you handle both failure and success at the same time. In a similar fashion, ZIO
effects also have several methods that allow you to handle both failure and success.
The first fold method, fold
, lets you non-effectfully handle both failure and success, by supplying a non-effectful handler for each case:
lazy val DefaultData: Array[Byte] = Array(0, 0)
val primaryOrDefaultData: UIO[Array[Byte]] =
openFile("primary.data").fold(
_ => DefaultData,
data => data)
The second fold method, foldM
, lets you effectfully handle both failure and success, by supplying an effectful (but still pure) handler for each case:
val primaryOrSecondaryData: IO[IOException, Array[Byte]] =
openFile("primary.data").foldM(
_ => openFile("secondary.data"),
data => ZIO.succeed(data))
Nearly all error handling methods are defined in terms of foldM
, because it is both powerful and fast.
In the following example, foldM
is used to handle both failure and success of the readUrls
method:
val urls: UIO[Content] =
readUrls("urls.json").foldM(
error => IO.succeed(NoContent(error)),
success => fetchContent(success)
)
Retrying
There are a number of useful methods on the ZIO data type for retrying failed effects.
The most basic of these is ZIO#retry
, which takes a Schedule
and returns a new effect that will retry the first effect if it fails, according to the specified policy:
import zio.clock._
val retriedOpenFile: ZIO[Clock, IOException, Array[Byte]] =
openFile("primary.data").retry(Schedule.recurs(5))
The next most powerful function is ZIO#retryOrElse
, which allows specification of a fallback to use, if the effect does not succeed with the specified policy:
openFile("primary.data").retryOrElse(
Schedule.recurs(5),
(_, _) => ZIO.succeed(DefaultData))
The final method, ZIO#retryOrElseEither
, allows returning a different type for the fallback.
For more information on how to build schedules, see the documentation on Schedule.
Next Steps
If you are comfortable with basic error handling, then the next step is to learn about safe resource handling.