ZIO.attempt: examples and documentation

ZIO 2 FAQ: How do I work with the ZIO.attempt function?

Solution

When you’re working with existing (legacy) Scala code that:

  • is synchronous, and
  • can throw an exception

wrap that code with ZIO.attempt. As you’re about to see, this creates a ZIO effect from that legacy code.

Not only does this create a ZIO effect, it also puts the exception in the ZIO “error channel,” i.e., the E parameter in the ZIO[R, E, A] type signature.

Example

When you’re working with ZIO, convert legacy synchronous code into a ZIO effect with ZIO.attempt. Here’s an example from the ZIO documentation:

import scala.io.StdIn

val readLine: ZIO[Any, Throwable, String] =
    ZIO.attempt(StdIn.readLine())

Because in certain situations StdIn.readLine() can throw an exception, we wrap it with ZIO.attempt.

In this example, the error type of the resulting readLine effect will be Throwable exceptions.

This usage is the first thing to know about ZIO.attempt.

ZIO.attempt vs ZIO.succeed

A second thing to know is to be clear about this:

  • If some existing legacy code absolutely CANNOT throw an exception, use ZIO.succeed instead.
  • Otherwise, wrap that code in ZIO.attempt, as I just showed.

For example, Scala’s println function can’t really fail unless there’s something really wrong with your computer, so you can wrap it in ZIO.succeed:

def printLine(line: String): ZIO[Any, Any, Unit] =
    ZIO.succeed(println(line))

Conversely, StdIn.readLine can potentially cause a problem --- especially in more advanced uses of it, where you try to read Int, Double and other values --- so we wrap it in ZIO.attempt:

val readLine: ZIO[Any, Throwable, String] =
    ZIO.attempt(StdIn.readLine())

That’s the second thing to know.

Getting the actual exception

Next, if you know the actual type of exception that can be thrown by your existing code, you can use the refineToOrDie method to return that specific exception rather than Throwable.

To demonstrate this, here’s the default ZIO.attempt return value:

ZIO.attempt(s.toInt)                         // ZIO[Any, Throwable, Int]

and here’s how you specify that you want the more-specific exception with refineToOrDie:

ZIO.attempt(s.toInt)
   .refineToOrDie[NumberFormatException]   // ZIO[Any, NumberFormatException, Int]

As shown, refineToOrDie lets me declare that I want the more-specific NumberFormatException in the E position (ZIO error channel) in this example.

Bad ways to work with Option, Try, and Either

In a related note, here are some BAD ways to use ZIO.attempt with Scala’s Option, Try, and Either types:

val optionValue: ZIO[Any, Throwable, Option[Int]]         = ZIO.attempt(Some(42))
val tryValue:    ZIO[Any, Throwable, Try[Int]]            = ZIO.attempt(Try(42))
val eitherValue: ZIO[Any, Throwable, Either[String, Int]] = ZIO.attempt(Right(42))

The word “bad” might be a little harsh, so maybe it’s better to say that those approaches are generally not preferred.

Preferred ways to work with Option, Try, and Either

Conversely, the preferred ways to work with Option, Try, and Either values are to NOT use ZIO.attempt, and instead use these functions:

  • ZIO.fromOption
  • ZIO.fromTry
  • ZIO.fromEither

Those functions are shown in these examples:

// Option
val optionValue: Option[Int] = Some(42)
val zioOption: ZIO[Any, Option[Nothing], Int] = ZIO.fromOption(optionValue)

// Try
val tryValue: Try[Int] = Success(42)
val zioTry: ZIO[Any, Throwable, Int] = ZIO.fromTry(tryValue)

// Either
val eitherValue: Either[String, Int] = Right(42)
val zioEither: ZIO[Any, String, Int] = ZIO.fromEither(eitherValue)

As you can see from those resulting ZIO type signatures, ZIO.fromOption, ZIO.fromTry, and ZIO.fromEither make for some nicer-looking code.

And even more importantly, when errors happen, they end up in the ZIO “error channel,” i.e., the E parameter in the ZIO[R, E, A] type signature. These two examples show the different results between the preferred and not-preferred solutions:

// PREFERRED:
val zioTry: ZIO[Any, Throwable, Int] = ZIO.fromTry(Try(42))

// NOT PREFERRED:
val tryValue: ZIO[Any, Throwable, Try[Int]] = ZIO.attempt(Try(42))

The first solution is preferred because any resulting errors are in the E error channel, and the Int value that you really want will be in the A position when no exceptions are thrown.

Summary: ZIO.attempt

In summary, I hope these ZIO.attempt examples and discussions are helpful. Key points to know are:

  • Wrap existing/legacy synchronous code with ZIO.attempt to turn that code into a ZIO effect.
  • When existing/legacy synchronous code CANNOT throw an exception, use ZIO.succeed instead.
  • When existing synchronous code yields Option, Try, or Either types, convert that code using ZIO.fromOption, ZIO.fromTry, and ZIO.fromEither, as shown.