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
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
When you start writing in ZIO, you have a lot of legacy APIs you are required to wrap or “lift” into
ZIO. The most common wrapper is
ZIO.effect which allows you to wrap synchronous code which has side-effects:
The problem starts when you don’t notice that you are “lifting” an asynchronous method that returns
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 a
Future[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.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
Any Scala developer is extremely accustomed to sprinkle their code with
println statements when debugging async code.
But this can be problematic when writing complex
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 a
println 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
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
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
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
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!
ZIO.Access instead of
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 —
One way we can inject a concrete implementation is by creating a trait that extends
ZLog with a concrete
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
The problem is that
ZIO.access expects a pure function without side-effects!
Meaning that no entry would be logged in our example (
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.
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.)
(Bonus tip) Use
tapError instead of
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
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