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.
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.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
, orEither
types, convert that code usingZIO.fromOption
,ZIO.fromTry
, andZIO.fromEither
, as shown.