5 (more) pitfalls to avoid when starting to work with ZIO

Natan Silnitsky
Wix Engineering
Published in
6 min readJan 17, 2020

My first ZIO post - 5 pitfalls to avoid when starting to work with ZIO was well received and I even got encouragement by some of my colleagues at Wix to share more of my beginner experiences with ZIO. So I’ve decided to write a sequel (like any current Hollywood screenwriter is easily persuaded to do).

If you are unfamiliar with pure functional programming and ZIO, you can find some useful links at the intro of my previous post

Photo by Ronaldo de Oliveira on Unsplash

All the following pitfalls are again from my first-hand experience with writing ZIO code, stumbling a bit here and there, and making a note of it. I hope these notes encourage you to learn more about how to write code with ZIO and avoid some of my mistakes.

1. Calling future inside ZIO.effect

When you start writing in ZIO, you have a lot of legacy APIs you are required to wrap or “lift” intoZIO. The most common wrapper is ZIO.effect which allows you to wrap synchronous code which has side-effects:
ZIO.effect(println("hello"))

The problem starts when you don’t notice that you are “lifting” an asynchronous method that returns Future.

For example, let’s say you are writing code for a service that processes a cart checkout request (See below). There are two client calls. The first call is to fetch the cart details, and the second call is to execute payment.

fetchCartOf method is synchronous: def fetchCartOf(order: Order): Cart
We assume (wrongly) that the pay method is also synchronous.

We don’t care about the type of the pay result, just that it completes successfully, and we know that any exceptions would be handled because ZIO.effect returns a Throwable in the error channel, which will make the service endpoint return a failure. Thus we decide to discard the pay result:
_ <- ZIO.effect(payClient.pay(cart, request.toOrder))

The compiler is happy, so we are happy, right? Wrong!

The actual signature of the pay method is:
def pay(cart: Cart, order: Order): Future[PaymentResult]

Meaning that ZIO is going to put in the success channel aFuture[PaymentResult], but what we really want to be returned is PaymentResult. The async action executed by Future is going to be invoked on a separate thread (outside the ZIO thread pool!) and the ZIO effect will complete without even waiting for the underlying async operation to complete.

For instance, the operation could complete with Failure and still our service endpoint will return a successful response (because Future[PaymentResult] is just a benign success value).

The way to solve this is simply to change the wrapping method from ZIO.effect to ZIO.fromFuture :

_ <- ZIO.fromFuture(_ => payClient.pay(cart, request.toOrder)).

Now our ZIO effect will complete only once the Future action does and any Future failure will be propagated to the ZIO error channel.

2. Combining Eager and Lazy code inside val

Any Scala developer is extremely accustomed to sprinkle their code with println statements when debugging async code.

But this can be problematic when writing complexvalstatements.
For instance, the following examples starts up drivers in test environment:

Now, let’s say there is some weird failure and we decide to add logs to debug:

Notice how we unintentionally added aprintln statement in line 3 without wrapping it in ZIO. the standard output for this app would be:

Starting to build mysql driver
Starting to build redis driver
>>>> going to create drivers
>>>> created mysql driver
>>>> started mysql driver
>>>> created redis driver

Notice how the prints are out of order because we forgot to wrap a few println statements with ZIO.effect. This can become very confusing for more complex cases…

The easiest way to avoid such issues is to use zio.console.putStrLn instead of ZIO.effect(println). This way, the compiler will not allow you to make this mistake.
And in general - try to have all the code wrapped inside ZIO effects.

3. Wrapping pattern matching (or conditional) expressions with ZIO inside a for comprehension

For comprehensions with ZIO can be quite tricky for beginners, especially when they contain pattern matching expressions.

The example below shows a pattern match expression wrapped in IO

The entire pattern match expression was wrapped in ZIO because the User and API is legacy:

But, there is a problem here, we missed the fact that the Media API was changed to return ZIO:

So now, only the User API is still functioning correctly, while the Media API is not, because it is wrapped in ZIO twice! (Once by the API, and the second time by the IO around the pattern match, giving us IO[Task[Unit]] which is unusable)

The lesson here is to never blindly wrap whole pattern match expressions in ZIO, instead each case should be evaluated separately and wrapped if needed:

4. Throwing FiberFailure instead of underlying Throwable

ZIO has great error tracing capabilities. When executing ZIO with Runtime.unsafeRun, a FiberFailure will be thrown that will include the useful information of the next LoC to run if the error did not happen:

Fiber:Id(1576169440339,10) was supposed to continue to:
a future continuation at com.demo.CheckoutService.
processCheckout(CheckoutService.scala:60)

Sometimes, for inter-op purposes we need our method to throw the underlying Throwable and NOT FiberFailure. Usually because the client code expects to catch certain exceptions.

But with the following example, the CheckoutService will throw a FiberFailure instead of PaymentError because it uses Runtime.unsafeRun:

Fear not, because with a new Runtime method soon-to-be-released in ZIO called unsafeRunTask, the original Throwable is thrown + all the goodies FiberFailure has are attached to the original Throwable stack trace!

5. Using ZIO.Access instead of ZIO.AccessM

The ZIO Environment (R parameter) allows you to inject dependencies into your ZIO based code. It is one of the key differentiators from Cats Effect. For more insights on the different ways you can inject dependencies in ZIO, I recommend this great article by Adam Warski.

Following is an example of a custom ZIO Environment dependency — ZLog

One way we can inject a concrete implementation is by creating a trait that extends ZLog with a concrete Logger type:

Which we then provide to ZIO runtime environment via the Cake Pattern:

Once your custom Dependency is in place, the way to use it is by calling ZIO.access :

The problem is that ZIO.access expects a pure function without side-effects!

Meaning that no entry would be logged in our example (warn and error return effects!).
Instead you should use ZIO.accessM which expects to get an effect.
The Compiler can’t help here because a ZIO effect is just a value like any other value.

warn and error could have just returned Unit — but then they could potentially throw exceptions (notice how we have wrapped the original slf4j methods with ZIO.effect and then called .ignore so they would not be able to fail.)

The same principle is also true for ZIO.when (ZIO.whenM) ZIO.fold (ZIO.foldM) and many others.
Make sure to make conscious decisions when you choose a ZIO helper method.

(Bonus tip) Use tapError instead of catchAll and ZIO.fail

Most web services will emit errors logs for debugging purposes. A standard practice for logging is to catch the error, log it and re-throw it in order to fail the request. ZIO has this capability with catchAll and ZIO.fail:

But a better method called tapError exists that has less boilerplate and conveys intent better:

_ <- payClient.pay(cart, request.toOrder)
.tapError(ex => UIO(logger.error(s"Payment Error: $ex")))

Thank you for reading!
If you’d like to get updates on my experiences with ZIO, follow me on Twitter and Medium.

You can also visit my website, where you will find my previous blog posts, talks I gave in conferences and open-source projects I’m involved with.

If anything is unclear or you want to point out something, please comment down below.

Checkout my blog series about migrating to Bazel build tool:
Migrating to Bazel from Maven or Gradle? 5 crucial questions you should ask yourself

--

--