Skip to main content
Version: 2.x

Introduction to Error Management in ZIO

ZIO's error management model is built around three interlocking types: Cause[E], Exit[E, A], and the error channel of ZIO[R, E, A]. Together they give every ZIO program a precise, type-safe account of every failure, unhandled exception, and fiber interruption that can occur — and a rich set of operators for recovering from each. Most readers encounter Cause[E] through recovery operators such as ZIO#catchAllCause and ZIO#sandbox rather than by constructing it directly; it is the runtime representation that the ZIO fiber runtime manages internally. Their structural shapes are:

sealed abstract class Cause[+E] // runtime error graph: failures, defects, interruptions
sealed trait Exit[+E, +A] // completed fiber result: Success or Failure(cause)
sealed trait ZIO[-R, +E, +A] // effect with a typed error channel E

Overview

The error management area is organized into six topics, each covering a distinct concern:

  • Three Types of Errors — the taxonomy of typed failures (Failure), untyped runtime exceptions (Defect), and catastrophic JVM errors (Fatal) that ZIO distinguishes at both the type-system and runtime levels.
  • Core Concepts — the typed-error guarantee, what it means for an effect to be "unexceptional", and how sequential versus parallel composition affects the error structure.
  • Error Channel Operations — mapping, filtering, refining, flipping, and otherwise transforming the error channel without necessarily recovering from it.
  • Recovering From Errors — catching typed failures, folding over results, retrying with a policy, sandboxing defects, timing out, and falling back to alternative effects.
  • Error Accumulation — collecting all failures from a collection of effects without short-circuiting, using ZIO.validate and related combinators.
  • Best Practices — modelling domain errors as sealed ADTs, keeping defects out of the typed channel, avoiding reflexive logging, and using union types (Scala 3) for lightweight error composition.

How They Work Together

The three core types interact through a single data flow: constructors create Cause leaf nodes, composition combines them into a Cause graph, the fiber runtime records the graph in an Exit value, and recovery operators intercept the graph at different granularities. Here is the step-by-step workflow:

  1. ZIO.fail(e) produces a Cause.Fail(e, trace) node; ZIO.die(t) produces Cause.Die(t, trace); ZIO.interrupt produces Cause.Interrupt(fiberId, trace).
  2. When two sequential effects both fail, their causes are joined with ++, producing Cause.Then(left, right) — a node that preserves temporal ordering.
  3. When two parallel effects both fail, their causes are joined with &&, producing Cause.Both(left, right) — a node that records concurrent failures without imposing an order.
  4. When a fiber finishes, the runtime wraps the outcome in an Exit: either Exit.Success(value) for a successful result or Exit.Failure(cause) carrying the full Cause[E] graph.
  5. Recovery operators such as ZIO#catchAll and ZIO#catchSome intercept only the typed E channel — they match Cause.Fail nodes but leave Cause.Die and Cause.Interrupt untouched.
  6. sandbox promotes the full Cause[E] into the typed error channel, producing ZIO[R, Cause[E], A], so any catch* operator can then pattern-match against Cause.Die, Cause.Interrupt, Cause.Then, or Cause.Both.
  7. ZIO#unsandbox (or the companion-object form ZIO.unsandbox(v)) reverts the sandbox, moving Cause[E] back out of the typed channel and restoring the effect's original E type.

The diagram below maps this data flow from construction through composition, fiber exit, and recovery:

┌────────────┐ ┌────────────┐ ┌────────────────┐
│ ZIO.fail │ │ ZIO.die │ │ ZIO.interrupt │
└─────┬──────┘ └─────┬──────┘ └───────┬────────┘
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────────┐
│ Cause.Fail │ │ Cause.Die │ │Cause.Interrupt │
└─────┬──────┘ └─────┬──────┘ └───────┬────────┘
│ (++) │ (&&) │
└────────────────┘ │
│ ┌─────────────────┘
▼ ▼
┌──────────────────────────────┐
│ Cause.Then / Cause.Both │
│ (sequential/parallel graph) │
└──────────────┬───────────────┘

┌──────────────┴───────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Exit.Failure(cause) │ │ Exit.Success(value) │
└──────────┬──────────┘ └──────────────────────┘


┌──────────────────────────────────────────────────┐
│ catchAll (typed E only) │
│ catchAllCause (full Cause[E] graph) │
│ sandbox → promotes Cause[E] to E channel │
└──────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────┐
│ recovered ZIO[R, E2, A] effect │
└──────────────────────────────────────────────────┘

foldCauseZIO is the primitive from which all catch* and fold* operators are derived — every recovery method in ZIO is ultimately expressed in terms of it.

Type Relationships:

  • Cause[E] is the runtime representation of all three error channels; the type parameter E covers only the typed-failure dimension, while Cause.Die and Cause.Interrupt nodes are always untyped.
  • Exit[E, A] is a sealed subtype of ZIO[Any, E, A] — an Exit value can be used anywhere a ZIO is expected without explicit lifting.
  • Exit.Failure(cause) wraps the full Cause[E] graph; Exit.Success(a) carries the result value of a successfully completed fiber.
  • Cause.Then preserves temporal ordering (sequential failures); Cause.Both preserves concurrency (parallel failures); Cause.Stackless suppresses stack-trace rendering for its sub-cause.
  • foldCauseZIO is the primitive from which all other catch* and fold* operators are derived; every other recovery combinator calls it internally.

To see these types working together, consider the sandbox/unsandbox pattern — the standard approach when you need to recover from a defect or inspect the full Cause structure:

import zio._

val effect: ZIO[Any, String, String] =
ZIO.succeed("primary result") *> ZIO.fail("Oh uh!")

val recovered: ZIO[Any, String, String] =
effect
.sandbox // ZIO[Any, Cause[String], String]
.catchSome { case Cause.Fail(_, _) =>
ZIO.succeed("fallback result") // matches typed failure only
}
.unsandbox // ZIO[Any, String, String]

After sandbox promotes the entire Cause[String] into the error channel, catchSome can pattern-match on Cause.Fail specifically — leaving any Cause.Die or Cause.Interrupt nodes untouched. Calling unsandbox at the end restores the effect's String error type.