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.validateand 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:
ZIO.fail(e)produces aCause.Fail(e, trace)node;ZIO.die(t)producesCause.Die(t, trace);ZIO.interruptproducesCause.Interrupt(fiberId, trace).- When two sequential effects both fail, their causes are joined with
++, producingCause.Then(left, right)— a node that preserves temporal ordering. - When two parallel effects both fail, their causes are joined with
&&, producingCause.Both(left, right)— a node that records concurrent failures without imposing an order. - When a fiber finishes, the runtime wraps the outcome in an
Exit: eitherExit.Success(value)for a successful result orExit.Failure(cause)carrying the fullCause[E]graph. - Recovery operators such as
ZIO#catchAllandZIO#catchSomeintercept only the typedEchannel — they matchCause.Failnodes but leaveCause.DieandCause.Interruptuntouched. sandboxpromotes the fullCause[E]into the typed error channel, producingZIO[R, Cause[E], A], so anycatch*operator can then pattern-match againstCause.Die,Cause.Interrupt,Cause.Then, orCause.Both.ZIO#unsandbox(or the companion-object formZIO.unsandbox(v)) reverts the sandbox, movingCause[E]back out of the typed channel and restoring the effect's originalEtype.
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 parameterEcovers only the typed-failure dimension, whileCause.DieandCause.Interruptnodes are always untyped.Exit[E, A]is a sealed subtype ofZIO[Any, E, A]— anExitvalue can be used anywhere aZIOis expected without explicit lifting.Exit.Failure(cause)wraps the fullCause[E]graph;Exit.Success(a)carries the result value of a successfully completed fiber.Cause.Thenpreserves temporal ordering (sequential failures);Cause.Bothpreserves concurrency (parallel failures);Cause.Stacklesssuppresses stack-trace rendering for its sub-cause.foldCauseZIOis the primitive from which all othercatch*andfold*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.