ZIO Effects: How to Handle Errors?

Reading Time: 3 minutes

Failure is an integral part of software applications. In practice, all components of an application can not be failure-proof. If a failure occurs and remains unhandled, it blows up everything. Hence, it is important to think about how to handle error scenarios while developing an application. This blog is an attempt to describe error handling in ZIO.

ZIO provides various means to handle and respond to failures. Some of them are:

Handling Error with Either:

ZIO provides a method called either on effects that produce an effect of type ZIO[R, Nothing, Either[E, A]]. As evident from the type, the resulting effect succeeds with an Either value.

  • If the effect fails, the result is in the form of Left
  • If the effect succeeds, the result is in the form of Right

Consider the code snippet below:

 val errorHandledEffect: URIO[Any, Either[String, Nothing]] =
      ZIO.fail("Deliberate failure").either
 val a: ZIO[Any, Nothing, String] = errorHandledEffect.map{
    case Right(value) => value
    case Left(ex) => "Default Response"
  }

The ZIO#fail the method is deliberately failing the effect, which is then handled using either. Since this effect will fail, the control flows through the Left block where the default value is plugged.

Handling Error with CatchAll:

While either models errors into Left and can return a default value, there may be requirements to catch and recover from all types of errors and return an effect with a different error type. This can be achieved by catchAll method. It is a callback that can create and return a new ZIO effect with a different type.

Consider the code snippet:

 val errorEffect: ZIO[Any, String, Nothing] = ZIO.fail("Some Failure")
 val attempt: ZIO[Any, Nothing, Int] = errorEffect.catchAll{_ =>
    ZIO.succeed(1)
  }

Handling Error with CatchSome:

In some cases, we may want to handle specific kinds of errors. In such a scenario, catchSome can be used. Consider the code snippet below:

 val: IO[IOException, Array[Byte]] = readFile("primary.txt").catchSome{
     case _ : FileNotFoundException => 
       readFile("backup.txt")
   }

Here, we are catching a specific type of error i.e. FileNotFoundException. This callback can also increase the scope of errors to a broader class. Consider the code snippet below:

 val readFileEffect = ZIO.fail(new FileNotFoundException("Missing file..."))
 val b: ZIO[Any, Exception, Nothing] = readFileEffect.catchSome{
   case a: Exception => ZIO.fail(new Exception("This is a forco images appear on this page. Add some!ed exception"))
 }

Handling Error with orElse:

In some cases, we may have to fall back on another effect in case one fails. This kind of effect is modeled by using orElse callback. Consider the code snippet:

 val primaryOrBackupData =
      readFile("primary.txt").orElse(readFile("backup.txt"))
  • If reading the primary.txt the file is a success, its content is returned in response
  • If reading the primary.txt the file is not a success, the attempt to read backup.txt is made.

Handling Error by Retry:

Suppose your application makes an API call. In case it fails, you may want to retry it before entering the error handling construct. ZIO provides different techniques to retry an effect that has failed. Consider the code snippet:

 def callAPI(path: String): ZIO[Any, Any, Any] = ???

 callAPI("https://dummyServer.com/somepath").retryN(3)

ZIO#retryN enable us to recall the effect a specified number of times if it fails. However, in some cases, we may need to provide some specific strategy for successive retries.

Consider the scenario where you need to connect to the database. If the connection fails one, there is no point in immediately retrying it again and again as it will only overload the already unresponsive server. It would be a wise choice to retry the connection with exponential backoff. ZIO lets you provide such strategies while retrying a failing effect.

Consider the code snippet below:

 def callAPI(path: String): ZIO[Any, Any, Any] = ???

 callAPI("https://dummyServer.com/somepath").retry(
     Schedule.exponential(100.milliseconds)
  )

ZIO provides more such schemes to retry the failing effects. For more details on different ways to schedule a retry, follow the official documentation on ZIO Schedule.

As a closing note, it is important to mention- though ZIO provides a wide range of options to use for error handling, it is important to wisely select and use them depending on the requirements of the application.

Further Reading:

Official documentation on ZIO Schedule: https://zio.dev/version-1.x/datatypes/misc/schedule/