1. Overview

In this tutorial, we’ll explore the options available in the ZIO framework to help us deal with errors in our ZIO applications. We’ll go through each function individually, trying examples by calling them on a ZIO Effect and then discussing anything to keep in mind when using the functions in our code.

2. Using .either()

The first function we’ll experiment with is .either(). This function will convert our effect into a Scala Either, turning a successful effect into a Right and a failure into a Left:

ZIO.succeed("result").either

This example converts the successful effect into a Right(“result”).

Now, let’s see a failure effect that gives us a Left(“error”):

ZIO.fail("error").either

3. Using .orElse()

If there’s a suitable default value we can return if our effect fails, we can use .orElse() to catch a failure and return an alternative result:

ZIO.fail("error").orElse(ZIO.succeed("default"))

This will convert our failure effect into a successful effect with the value of “default”.

4. Using .catchAll()

To recover from any failure from our effect, we can use .catchAll(). This also gives us the opportunity to log any details about the failure that occurred:

ZIO.fail("error").catchAll(_ =>
  for {
    _ <- ZIO.logError("Some error occurred")
    resource <- ZIO.succeed("default")
  } yield resource
)

Since .catchAll() handles all failures that could occur, using it can change the error type of our ZIO to a different value or even Nothing if it’s not possible for a failure state to occur. In this example, our effect would go from ZIO[Any, String, String] to ZIO[Any, Nothing, String].

5. Using .catchSome()

If we can recover from some failure states, but not all of them, we can use .catchSome() to handle whichever error types we want to:

ZIO.fail(new IOException()).catchSome { case _: IOException =>
  ZIO.logError("Some error occurred")
  ZIO.succeed("default")
}

This will catch any IOException returned by a failure effect, but any other type of failure will remain untouched. As a result, the type of our effect must remain unchanged when using .catchSome().

6. Using .fold()

When handling both a successful and failure state differently, we can use .fold(). This will allow us to do one thing when we have a failure and something else in the successful case of our effect:

ZIO.succeed("result").fold(
  fail => "some default",
  success => success
)

Since .fold() is called on a successful effect here, we’ll return the value of success. We could also do whatever processing we need to do on this value instead:

ZIO.succeed("result").fold(
  fail => "some default",
  success => "This effect was: " + success
)

In this example, we’re doing additional processing on our successful state, and it’ll return “This effect was: result”. Now, let’s see what happens if we do the same on a failure effect:

ZIO.fail("error").fold(
  fail => "some default",
  success => success
)

Here, we’ve called .fold() on a failure effect and caught the error. This means this snippet will return a result of “some default”.

7. Using .foldZIO()

In the previous section, we covered .fold(), which will return us a literal value from our effects. If we need to return an effect from our fold, we can use .foldZIO():

ZIO.succeed("result").foldZIO(
  fail => ZIO.succeed("some default"),
  success => ZIO.succeed(success)
)

This snippet implements precisely the same logic as the example in the previous section. However, it returns a successful effect for both cases.

8. Using .retry()

When we’re accessing external resources in our application code, they’re not always accessible on the first attempt. Therefore, sometimes we need to make a number of attempts before we result in a failure. To do this in ZIO, we can use .retry().

When using .retry(), we need to provide a Schedule with the number of times we want to attempt to access the resource:

ZIO.fail("error").retry(Schedule.recurs(3))

This is a static example, so this will always result in the same failure result, but in a real-world example, this will reattempt the effect up to three times, as defined by passing .retry() the Schedule of Schedule.recurs(3).

9. Using .retryOrElse()

The .retry() function we explored in the previous section is very useful. To build on that, we’ll explore .retryOrElse(), which allows us to return a default successful effect if all attempts fail during the retry:

ZIO.fail("error").retryOrElse(Schedule.recurs(3), (_, _: Long) => ZIO.succeed("default"))

This snippet, again, will retry the effect three times while resulting in a failed effect. On the failure of the third attempt, the result will be a ZIO.succeed(“default”).

10. Conclusion

In this article, we’ve explored the functions available to us in ZIO to help us handle errors in our code. We’ve tried each function using a coded example and have discussed when we should use each of them in our code.

As always, the sample code used in this article is available over on GitHub.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.