Skip to main content
Version: 1.0.18

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.