Understanding the “Found ZIO.Task Required ZIO.ZIO” dilemma
In my “beginner ZIO” journey, I just hit one of those “light bulb moments” where something that has stymied me countless times suddenly makes sense, and hopefully this little article can help anyone with a similar scenario.
Supposed I want to do something in a nice ZIO-ish failsafe way like reading a file. Let’s consider the really simple line:
val myData: ZIO[Any, IOException, Iterator[String] =
ZIO.attempt(Source.fromFile("foo.txt").getLines)
This makes sense: I know that the only likely failure form a Source.fromFile
function would be an IOException
so I’m declaring that as the error channel. But the program won’t compile, and instead I get this error message:
Type Mismatch Error:
Found: zio.Task[Iterator[String]]
Required: zio.ZIO[Any, java.io.IOException, Iterator[String]]
I’m confused, because I know a ZIO Task is just a ZIO that has no dependencies and that uses typical Java Throwable items for the error channel. What’s going on?
Well, to understand this, it’s worthwhile to understand a little bit more nuance to the ZIO error handling machinery. In a nutshell, ZIO thinks of errors as falling under two categories: things that you expect that you might have a strategy for recovering from, and those unexpected things that could also throw errors, e.g. runtime errors like Out of Memory exceptions, etc. This second category of errors is referred to as Defects.
Let’s go back to Type Mismatch Error message, and let’s expand the Task
type alias back to a ZIO
so we can match apples-to-apples:
Type Mismatch Error:
Found: zio.ZIO[Any, Throwable, Iterator[String]]
Required: zio.ZIO[Any, IOException, Iterator[String]]
Now it should be clear that the compiler is complaining because we told it that the only expected error class would be IOException
(or any potential subtype), but when our code called ZIO.attempt
the compiler only know that any Throwable
could be thrown. As the developer, I may know that my Source.fromFile
method is only likely to throw an IOException, but how do I tell this to the compiler?
The ZIO answer is that I need to refine my error channel from the broad Throwable
to the narrower IOException
. The way you can do this is to use the .refineOrDie
method, where you can provide a Partial Function (i.e. one or more case statements) that will catch the expected errors and remap them to our target. The way we would do this with our original code would be to say:
val myData: ZIO[Any, IOException, Iterator[String] =
ZIO.attempt(Source.fromFile("foo.txt").getLines)
.refineOrDie {
case e: IOException => e
}
In this case, if our ZIO.attempt
clause throws anything other than an IOException
then the ultimate unsafeRun
will die immediately. A convenient way to do the same simple refinement is to use the .refineToOrDie[E]
method like this:
val myData: IO[IOException, Iterator[String] =
ZIO.attempt(Source.fromFile("foo.txt").getLines)
.refineToOrDie[IOException]
By the way, I learned the nuances of the ZIO Error model by reading the Zionomicon book, which is currently available in an Early Access form. The book is not free, but it really does a great job of providing an in-depth understanding of the entire ZIO ecosystem.