Understanding the “Found ZIO.Task Required ZIO.ZIO” dilemma

Murray Todd Williams
3 min readFeb 28, 2022

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.

--

--

Murray Todd Williams

Life-long learner, foodie and wine enthusiast, living in Austin, Texas.