ZIO 2: Solution to "ZIO.cond not working" (code not running)

I just wrote the following ZIO 2 question about how to use ZIO.cond to a friend, and get the answer shown. I’ve also added in my own comments where they make sense.

ZIO.done question

Hey, I’m trying to understand why the failWithMsgEffect doesn’t seem to get run in the following code example?

I have learned that there are better ways to handle this, but I’ve found that if I don’t understand something like this, it will come back to bite me later. Here’s the code:

//> using scala "3"
//> using lib "dev.zio::zio::2.1.0"
package zio_done

import zio.*
import zio.Console.*

object ZioMinimalDoneTest extends ZIOAppDefault:

    // NOTE: this never seems to get run
    val failWithMsgEffect = 
        printLineError("Usage: yada yada...").flatMap { _ =>
            ZIO.fail(new Exception("Usage error"))
        }

    val blueprint =
        for
            args <- ZIOAppArgs.getArgs
            rez  <- ZIO.cond(args.size >= 1, (), failWithMsgEffect)
            _    <- printLine(s"\nfor is still running\n")
        yield
            args(0)

    def run = blueprint.foldZIO(
        failure => printLineError(s"FAILURE = $failure"),
        success => printLine(     s"SUCCESS = $success")
    )

If I run that WITH a command-line argument, I see the "for is still running" output and get the "SUCCESS" message, but if I run it WITHOUT any command-line arguments all I see is this output:

FAILURE = FlatMap(zio_done.ZioMinimalDoneTest.failWithMsgEffect(ZioDoneTestMinimal.scala:13),Stateful(zio_done.ZioMinimalDoneTest.failWithMsgEffect(ZioDoneTestMinimal.scala:11),zio.FiberRef$unsafe$$anon$2$$Lambda$43/0x000000030017ae10@2d285785),zio_done.ZioMinimalDoneTest$$$Lambda$44/0x000000030017be48@40aa7f74)

I don’t see any of the strings in failWithMsgEffect. I’ve tried implementing failWithMsgEffect with a for expression and with *>, but as far as I can tell, it never gets invoked. What am I doing wrong?

Solution

The problem you’re running into is due to the behavior of ZIO.cond. The way it works is that it WILL NOT execute the effect in the failure case — which is failWithMsgEffect in your code — if the condition fails. Instead, it directly fails with the effect itself as a failure, not its result. So basically failWithMsgEffect is not being run, as you have noted, but instead, it’s being passed as a value to indicate failure.

To make it clearer, ZIO.cond works like this:

  • If the condition is true, it produces the success value.
  • If the condition is false, it produces the failure value. (It fails with the given effect as the failure content/value, but does not execute that effect.)

To get what you want, which is to execute failWithMsgEffect when the condition is false, you need to explicitly run the effect when the condition fails. You do this by using more explicit conditional handling rather than ZIO.cond.

For instance, you might be able to use flatMap, or in this case an if condition will due the trick:

val blueprint =
    for
        args <- ZIOAppArgs.getArgs
        rez  <- if args.size >= 1 then ZIO.succeed(()) else failWithMsgEffect
        _    <- printLine(s"\nfor is still running\n")
    yield
        ()

Verification (The final, corrected code)

I want to note that I verified this solution, and this is my final, corrected code:

//> using scala "3"
//> using lib "dev.zio::zio::2.1.0"
package zio_done

import zio.*
import zio.Console.*

object ZioMinimalDoneTest extends ZIOAppDefault:

    val failWithMsgEffect = 
        printLineError("Usage: yada yada...").flatMap { _ =>
            ZIO.fail(new Exception("Usage error"))
        }

    val blueprint =
        for
            args <- ZIOAppArgs.getArgs
            rez  <- if args.size >= 1 then ZIO.succeed(()) else failWithMsgEffect
            _    <- printLine(s"\nfor is still running\n")
        yield
            args(0)

    def run = blueprint.foldZIO(
        failure => printLineError(s"FAILURE = $failure"),
        success => printLine(     s"SUCCESS = $success")
    )

As shown, I use an if condition inside my blueprint value, and everything now works as I expected. Here’s the success case output:

$ scala-cli ZioDoneTestMinimal.scala -- 42

for is still running

SUCCESS = 42

And here’s the failure case output:

$ scala-cli ZioDoneTestMinimal.scala
Usage: yada yada...
FAILURE = java.lang.Exception: Usage error

Note that in this link/example, the creator of that code throws an Exception for the failure/error case.

Summary

In summary, if you are having problems understanding how ZIO.cond works, I hope this example and description is helpful.