Skip to main content
zio-cats

Error Handling

Step 3 of 15

Error Handling

Both ZIO and Cats Effect provide powerful error handling, but with different approaches to error types.

Catching Errors

Cats Effect
import cats.effect.IO

// Handle all errors
val handled: IO[Int] =
  IO.raiseError[Int](new Exception("oops"))
    .handleError(_ => 0)

// Handle with effect
val handledWith: IO[Int] =
  IO.raiseError[Int](new Exception("oops"))
    .handleErrorWith(_ => IO.pure(0))

handleError / handleErrorWith

ZIO
import zio._

// Catch all errors
val handled: UIO[Int] =
  ZIO.fail("oops")
    .catchAll(_ => ZIO.succeed(0))

// Catch specific errors
val catchSome: IO[String, Int] =
  ZIO.fail("network")
    .catchSome {
      case "network" => ZIO.succeed(-1)
    }

catchAll / catchSome

Recovering

Cats Effect
import cats.effect.IO

// Recover with partial function
val recovered: IO[Int] =
  IO.raiseError[Int](new Exception("fail"))
    .recover {
      case _: Exception => 0
    }

// Recover with effect
val recoveredWith: IO[Int] =
  IO.raiseError[Int](new Exception("fail"))
    .recoverWith {
      case _: Exception => IO.pure(0)
    }

recover / recoverWith

ZIO
import zio._

// Fallback to alternative
val recovered: UIO[Int] =
  ZIO.fail("error")
    .orElse(ZIO.succeed(0))

// Fold error and success
val folded: UIO[Int] =
  ZIO.fail("error")
    .fold(_ => 0, identity)

orElse / fold

Effectful Fold

foldZIO is the fundamental error handling operator in ZIO. It handles both error and success cases with effects.

Cats Effect
import cats.effect.IO

// Fold with effectful branches (need attempt first)
val folded: IO[String] =
  IO.raiseError[Int](new Exception("fail"))
    .attempt
    .flatMap {
      case Left(e) => IO.pure(s"Error: ${e.getMessage}")
      case Right(v) => IO.pure(s"Success: $$v")
    }

Manual pattern matching on attempt

ZIO
import zio._

// foldZIO - effectful fold over error/success
val folded: IO[String, String] =
  ZIO.fail("error")
    .foldZIO(
      error => ZIO.succeed(s"Error: $$error"),
      value => ZIO.succeed(s"Success: $$value")
    )

foldZIO - handles both cases effectfully

Full Cause Handling

ZIO distinguishes between errors (anticipated failures in the error type) and defects (unanticipated failures like throwing exceptions). Use foldCauseZIO when you need to handle both.

Cats Effect
import cats.effect.IO

// CE always uses Throwable - no distinction
val handled: IO[Int] =
  IO.raiseError[Int](new Exception("fail"))
    .handleErrorWith(e => IO.pure(0))

// Defects (like division by zero) are indistinguishable
val defect: IO[Int] = IO.delay(1 / 0)
  .handleErrorWith(_ => IO.pure(-1))

All failures are Throwable - typed vs untyped

ZIO
import zio._

// foldCauseZIO - handle errors AND defects
val handled: UIO[Int] =
  ZIO.attempt(1 / 0)  // Could defect!
    .foldCauseZIO(
      cause => ZIO.succeed(cause.failures.headOption.map(_.getMessage.length).getOrElse(0)),
      value => ZIO.succeed(value)
    )

// Recover from defects - returns error count
val recoverDefects: UIO[Int] =
  ZIO.die(new RuntimeException("defect"))
    .foldCauseZIO(
      cause => cause.defects match {
        case Nil => ZIO.succeed(-1)  // No defects
        case ds  => ZIO.succeed(ds.length) // Count defects
      },
      _ => ZIO.succeed(0)
    )

foldCauseZIO - full Cause[E] with error/defect info

TIP:

In ZIO, errors are in your type signature and defects are not. Use foldZIO for errors and foldCauseZIO when you need to handle defects (like logging uncaught exceptions).

Fallback Combinators

ZIO provides several orElse variants for fallback behavior.

Cats Effect
import cats.effect.IO

// Fallback to alternative effect
val fallback: IO[Int] =
  IO.raiseError[Int](new Exception("fail"))
    .handleErrorWith(_ => IO.pure(0))

// Recover with specific value on error
val recover: IO[Int] =
  IO.raiseError[Int](new Exception("fail"))
    .recover { case _ => 0 }

handleErrorWith / recover

ZIO
import zio._

// orElse - fallback to alternative effect
val fallback: UIO[Int] =
  ZIO.fail("error").orElse(ZIO.succeed(0))

// orElseFail - replace error type
val mayFail: IO[String, Int] =
  ZIO.fail(42).orElseFail("replaced error")

// orElseEither - switch to Either result type
val either: UIO[Either[String, Int]] =
  ZIO.fail("error").orElseEither(ZIO.succeed(42))

// orElse on failable effect - original wins on success
val originalWins: UIO[Int] =
  ZIO.attempt(42).orElse(ZIO.succeed(0))

orElse / orElseFail / orElseEither

Transforming Errors

Cats Effect
import cats.effect.IO
import cats.syntax.all._

// Transform error type
val adapted: IO[Int] =
  IO.raiseError[Int](new Exception("error"))
    .adaptError {
      case e: Exception =>
        new RuntimeException(e.getMessage)
    }

adaptError - transform Throwable

ZIO
import zio._

// Map error to different type
val mapped: IO[String, Int] =
  ZIO.fail(new Exception("error"))
    .mapError(_.getMessage)

// Refine error type
val refined: IO[IllegalArgumentException, Int] =
  ZIO.fail(new Exception("oops"))
    .refineOrDie {
      case e: IllegalArgumentException => e
    }

mapError / refineOrDie - typed error transformation

TIP:

ZIO's typed errors let you narrow or widen error types at compile time. Cats Effect always uses Throwable, so you transform within that type.

Either-Based Handling

Cats Effect
import cats.effect.IO

// Convert error to Either
val asEither: IO[Either[Throwable, Int]] =
  IO.raiseError[Int](new Exception("fail"))
    .attempt

// Convert Either back to IO
val fromEither: IO[Int] =
  IO.fromEither(Left(new Exception("fail")))

attempt / fromEither

ZIO
import zio._

// Convert to Either
val asEither: UIO[Either[String, Int]] =
  ZIO.fail("error").either

// Convert from Either
val fromEither: IO[String, Int] =
  ZIO.fromEither(Left("error"))

// Absolute (merge error into success)
val merged: UIO[Any] =
  ZIO.fail("error").merge

either / fromEither / merge

Ensuring (Finally)

Cats Effect
import cats.effect.IO

// Always run finalizer
val guaranteed: IO[Int] =
  IO.pure(42)
    .guarantee(IO.println("cleanup"))

// On error only
val onError: IO[Int] =
  IO.raiseError[Int](new Exception("fail"))
    .onError(e => IO.println(s"Error: $$e"))

guarantee / onError

ZIO
import zio._

// Always run finalizer
val ensured: UIO[Int] =
  ZIO.succeed(42)
    .ensuring(ZIO.succeed(println("cleanup")))

// On error only
val onError: IO[String, Int] =
  ZIO.fail("fail")
    .onError(e => ZIO.succeed(println(s"Error: $$e")))

ensuring / onError

Next Steps

Error handling patterns differ mainly in typing. Let's look at functional composition next.

Next: Map/FlatMap →