Skip to content
AgileLogics
6 min read

Functional Effects: Cats Effect and ZIO

Prerequisites: Futures and concurrency, Higher-kinded types

An effect system represents side effects — I/O, time, randomness, concurrency — as values that describe what to do. Nothing happens until the program runs the description at "the end of the world".

The core idea

// A tiny IO, to show the idea
final case class IO[A](run: () => A):
  def map[B](f: A => B): IO[B] = IO(() => f(run()))
  def flatMap[B](f: A => IO[B]): IO[B] = IO(() => f(run()).run())

object IO:
  def delay[A](a: => A): IO[A] = IO(() => a)

def say(msg: String): IO[Unit] = IO.delay(println(msg))

@main def effects(): Unit =
  val program = for
    _ <- say("Hello")
    _ <- say("World")
  yield ()

  println("Nothing has printed yet")
  program.run()
  program.run()
Nothing has printed yet
Hello
World
Hello
World

program is a value: building it printed nothing, and it can be run twice, retried, or combined with others. Real effect systems build on this idea with cancellation, resource safety, and lightweight threads (fibers).

Cats Effect

// illustrative
//> using dep org.typelevel::cats-effect:3.6.1
import cats.effect.{IO, IOApp}
import cats.syntax.all.*
import scala.concurrent.duration.*

object Main extends IOApp.Simple:
  def fetch(id: Int): IO[String] =
    IO.sleep(200.millis) *> IO.pure(s"result $id")

  val run: IO[Unit] =
    for
      results <- (1 to 5).toList.parTraverse(fetch)   // concurrently
      _       <- IO.println(results.mkString(", "))
      winner  <- IO.race(fetch(1), IO.sleep(1.second))
      _       <- IO.println(winner)
    yield ()
result 1, result 2, result 3, result 4, result 5
Left(result 1)

ZIO

// illustrative
//> using dep dev.zio::zio:2.1.17
import zio.*

object Main extends ZIOAppDefault:
  def fetch(id: Int): Task[String] = ZIO.succeed(s"result $id").delay(200.millis)

  val run =
    for
      results <- ZIO.foreachPar(1 to 5)(fetch).withParallelism(3)
      _       <- Console.printLine(results.mkString(", "))
      value   <- fetch(9).timeout(1.second).retry(Schedule.recurs(3))
      _       <- Console.printLine(value)
    yield ()

Both libraries offer:

  • Fibers — millions of lightweight threads, with safe cancellation.
  • Resource safety — Resource / ZIO.acquireRelease guarantee cleanup even on errors or cancellation.
  • Concurrency tools — parallel traversal, races, timeouts, retries, queues, and concurrent state.
  • Ecosystems — http4s, fs2, Doobie, and Skunk for Cats Effect; ZIO HTTP, ZIO Streams, and ZIO JDBC for ZIO.

Cats Effect leans on type classes and pairs with the Cats library; ZIO has built-in typed errors and dependency injection (ZLayer). Teams usually pick one and use its ecosystem consistently.