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.succeedinstead. - 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.fromOptionZIO.fromTryZIO.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.
| this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
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.attemptto turn that code into a ZIO effect. - When existing/legacy synchronous code CANNOT throw an exception, use
ZIO.succeedinstead. - When existing synchronous code yields
Option,Try, orEithertypes, convert that code usingZIO.fromOption,ZIO.fromTry, andZIO.fromEither, as shown.