0
# Error Handling
1
2
ZIO provides a comprehensive error model with rich failure causes and composable retry/repeat scheduling for building resilient applications that gracefully handle failures and recover automatically.
3
4
## Capabilities
5
6
### Cause - Rich Error Model
7
8
ZIO's Cause type provides a rich representation of failure modes including typed errors, defects, and interruptions.
9
10
```scala { .api }
11
/**
12
* Represents the cause of a ZIO effect failure with rich error information
13
*/
14
sealed abstract class Cause[+E] {
15
/** Extract all typed failures from this cause */
16
def failures: List[E]
17
18
/** Extract all defects (untyped failures) from this cause */
19
def defects: List[Throwable]
20
21
/** Extract all fiber IDs that caused interruption */
22
def interruptors: Set[FiberId]
23
24
/** Get the first failure if any */
25
def failureOption: Option[E]
26
27
/** Get the first defect if any */
28
def dieOption: Option[Throwable]
29
30
/** Get the first interruption cause if any */
31
def interruptOption: Option[FiberId]
32
33
/** Extract either the failure or the remaining cause */
34
def failureOrCause: Either[E, Cause[Nothing]]
35
}
36
```
37
38
### Cause Construction and Composition
39
40
Create and combine causes to represent complex failure scenarios.
41
42
```scala { .api }
43
/**
44
* Combine two causes in parallel (both happened simultaneously)
45
*/
46
def &&[E1 >: E](that: Cause[E1]): Cause[E1]
47
48
/**
49
* Combine two causes sequentially (second happened after first)
50
*/
51
def ++[E1 >: E](that: Cause[E1]): Cause[E1]
52
53
/**
54
* Transform the typed error part of the cause
55
*/
56
def map[E1](f: E => E1): Cause[E1]
57
58
/**
59
* FlatMap over the cause structure
60
*/
61
def flatMap[E2](f: E => Cause[E2]): Cause[E2]
62
63
/**
64
* Replace all failures with a constant error
65
*/
66
def as[E1](e: => E1): Cause[E1]
67
68
// Factory methods
69
object Cause {
70
/** Empty cause (no failure) */
71
val empty: Cause[Nothing]
72
73
/** Create a cause from a typed failure */
74
def fail[E](error: E, trace: StackTrace = StackTrace.none): Cause[E]
75
76
/** Create a cause from a defect (untyped failure) */
77
def die(defect: Throwable, trace: StackTrace = StackTrace.none): Cause[Nothing]
78
79
/** Create a cause from fiber interruption */
80
def interrupt(fiberId: FiberId, trace: StackTrace = StackTrace.none): Cause[Nothing]
81
}
82
```
83
84
**Usage Examples:**
85
86
```scala
87
import zio._
88
89
// Creating different types of causes
90
val typedFailure = Cause.fail("Invalid input")
91
val defectCause = Cause.die(new RuntimeException("Unexpected error"))
92
val interruptCause = Cause.interrupt(FiberId.make(123))
93
94
// Combining causes
95
val combinedCause = Cause.fail("Error 1") && Cause.fail("Error 2")
96
val sequentialCause = Cause.fail("First error") ++ Cause.die(new Exception("Then this"))
97
98
// Analyzing cause structure
99
val analyzeFailure = (cause: Cause[String]) => {
100
val failures = cause.failures
101
val defects = cause.defects
102
val interruptions = cause.interruptors
103
104
s"Failures: ${failures.mkString(", ")}, Defects: ${defects.size}, Interruptions: ${interruptions.size}"
105
}
106
```
107
108
### Cause Predicates and Filtering
109
110
Query and filter causes to understand and handle specific failure types.
111
112
```scala { .api }
113
/**
114
* Check if the cause is empty (no failures)
115
*/
116
def isEmpty: Boolean
117
118
/**
119
* Check if cause contains typed failures
120
*/
121
def isFailure: Boolean
122
123
/**
124
* Check if cause contains defects
125
*/
126
def isDie: Boolean
127
128
/**
129
* Check if cause contains interruptions
130
*/
131
def isInterrupted: Boolean
132
133
/**
134
* Check if cause contains only interruptions
135
*/
136
def isInterruptedOnly: Boolean
137
138
/**
139
* Keep only defects, removing failures and interruptions
140
*/
141
def keepDefects: Option[Cause[Nothing]]
142
143
/**
144
* Remove all typed failures
145
*/
146
def stripFailures: Cause[Nothing]
147
148
/**
149
* Remove specific defects matching a partial function
150
*/
151
def stripSomeDefects(pf: PartialFunction[Throwable, Any]): Option[Cause[E]]
152
153
/**
154
* Filter the cause structure
155
*/
156
def filter(p: Cause[E] => Boolean): Cause[E]
157
```
158
159
**Usage Examples:**
160
161
```scala
162
// Cause analysis and filtering
163
val handleCause = (cause: Cause[AppError]) => {
164
if (cause.isInterruptedOnly) {
165
Console.printLine("Operation was cancelled")
166
} else if (cause.isFailure) {
167
Console.printLine(s"Business logic error: ${cause.failures.head}")
168
} else if (cause.isDie) {
169
Console.printLineError(s"System error: ${cause.defects.head.getMessage}")
170
} else {
171
Console.printLine("No errors occurred")
172
}
173
}
174
175
// Selective error handling
176
val recoverableErrors = cause.stripSomeDefects {
177
case _: TimeoutException => () // Remove timeout defects
178
case _: IOException => () // Remove IO defects
179
}
180
```
181
182
### Cause Debugging and Presentation
183
184
Tools for debugging and presenting cause information to users and developers.
185
186
```scala { .api }
187
/**
188
* Get the stack trace associated with this cause
189
*/
190
def trace: StackTrace
191
192
/**
193
* Get all stack traces in the cause
194
*/
195
def traces: List[StackTrace]
196
197
/**
198
* Add a stack trace to the cause
199
*/
200
def traced(trace: StackTrace): Cause[E]
201
202
/**
203
* Remove stack traces from the cause
204
*/
205
def untraced: Cause[E]
206
207
/**
208
* Convert cause to a human-readable string
209
*/
210
def prettyPrint: String
211
212
/**
213
* Convert cause to a single Throwable (for interop)
214
*/
215
def squash(implicit ev: E IsSubtypeOfError Throwable): Throwable
216
217
/**
218
* Convert cause to Throwable using custom function
219
*/
220
def squashWith(f: E => Throwable): Throwable
221
222
/**
223
* Get annotations attached to the cause
224
*/
225
def annotations: Map[String, String]
226
227
/**
228
* Get execution spans in the cause
229
*/
230
def spans: List[LogSpan]
231
232
/**
233
* Add annotations to the cause
234
*/
235
def annotated(anns: Map[String, String]): Cause[E]
236
237
/**
238
* Add execution spans to the cause
239
*/
240
def spanned(spans: List[LogSpan]): Cause[E]
241
```
242
243
**Usage Examples:**
244
245
```scala
246
// Debug information extraction
247
val debugFailure = (cause: Cause[String]) => for {
248
_ <- Console.printLineError("=== Failure Analysis ===")
249
_ <- Console.printLineError(cause.prettyPrint)
250
_ <- Console.printLineError(s"Stack traces: ${cause.traces.size}")
251
_ <- Console.printLineError(s"Annotations: ${cause.annotations}")
252
} yield ()
253
254
// Convert for external systems
255
val convertToThrowable = (cause: Cause[AppError]) => {
256
cause.squashWith {
257
case ValidationError(msg) => new IllegalArgumentException(msg)
258
case DatabaseError(msg) => new SQLException(msg)
259
case NetworkError(msg) => new IOException(msg)
260
}
261
}
262
```
263
264
### Schedule - Retry and Repeat Logic
265
266
Composable scheduling policies for retry and repeat operations with various timing strategies.
267
268
```scala { .api }
269
/**
270
* A composable schedule for retrying and repeating operations
271
* - Env: Environment required for scheduling decisions
272
* - In: Input type (usually the error type for retries)
273
* - Out: Output type (usually the retry count or delay)
274
*/
275
trait Schedule[-Env, -In, +Out] {
276
type State
277
278
/** Initial state of the schedule */
279
def initial: State
280
281
/** Determine the next step given current time, input, and state */
282
def step(now: OffsetDateTime, in: In, state: State): ZIO[Env, Nothing, (State, Out, Decision)]
283
}
284
285
/**
286
* Decision whether to continue or stop scheduling
287
*/
288
sealed trait Decision
289
object Decision {
290
case class Continue(interval: Intervals) extends Decision
291
case object Done extends Decision
292
}
293
```
294
295
### Schedule Composition Operators
296
297
Combine schedules using various operators to create complex retry policies.
298
299
```scala { .api }
300
/**
301
* Intersection - run both schedules and continue only if both want to continue
302
*/
303
def &&[Env1 <: Env, In1 <: In, Out2](that: Schedule[Env1, In1, Out2]): Schedule[Env1, In1, (Out, Out2)]
304
305
/**
306
* Union - run both schedules and continue if either wants to continue
307
*/
308
def ||[Env1 <: Env, In1 <: In, Out2](that: Schedule[Env1, In1, Out2]): Schedule[Env1, In1, (Out, Out2)]
309
310
/**
311
* Sequential composition - run this schedule, then that schedule
312
*/
313
def ++[Env1 <: Env, In1 <: In, Out2 >: Out](that: Schedule[Env1, In1, Out2]): Schedule[Env1, In1, Out2]
314
315
/**
316
* Compose outputs - pipe output of this schedule as input to next schedule
317
*/
318
def >>>[Env1 <: Env, Out2](that: Schedule[Env1, Out, Out2]): Schedule[Env1, In, Out2]
319
320
/**
321
* Either composition - try this schedule, fallback to that schedule
322
*/
323
def |||[Env1 <: Env, In1 <: In, Out2](that: Schedule[Env1, In1, Out2]): Schedule[Env1, In1, Either[Out, Out2]]
324
```
325
326
**Usage Examples:**
327
328
```scala
329
// Complex retry policy
330
val resilientRetry =
331
Schedule.exponential(100.millis) // Exponential backoff
332
.jittered // Add randomness
333
&& Schedule.recurs(5) // Max 5 attempts
334
&& Schedule.recurWhile[Throwable](_.isInstanceOf[TemporaryException])
335
336
// Fallback strategy
337
val retryWithFallback =
338
Schedule.exponential(1.second) && Schedule.recurs(3) // Primary strategy
339
++ Schedule.spaced(30.seconds) && Schedule.recurs(2) // Fallback strategy
340
341
// Complex condition-based retry
342
val smartRetry =
343
Schedule.recurWhile[DatabaseError] {
344
case ConnectionTimeout => true
345
case DeadlockDetected => true
346
case ConstraintViolation => false
347
} && Schedule.exponential(500.millis).jittered
348
```
349
350
### Common Schedule Patterns
351
352
Pre-built schedules for common retry and repeat scenarios.
353
354
```scala { .api }
355
// Basic schedules
356
/** Run forever */
357
val forever: Schedule[Any, Any, Long]
358
359
/** Run exactly once */
360
def once: Schedule[Any, Any, Unit]
361
362
/** Run n times */
363
def recurs(n: Long): Schedule[Any, Any, Long]
364
365
/** Always succeed with constant value */
366
def succeed[A](a: => A): Schedule[Any, Any, A]
367
368
/** Identity schedule (pass input as output) */
369
def identity[A]: Schedule[Any, A, A]
370
371
// Time-based schedules
372
/** Fixed duration delay */
373
def duration(duration: Duration): Schedule[Any, Any, Duration]
374
375
/** Fixed interval spacing */
376
def spaced(duration: Duration): Schedule[Any, Any, Long]
377
378
/** Fixed rate (accounting for execution time) */
379
def fixed(interval: Duration): Schedule[Any, Any, Long]
380
381
/** Exponential backoff */
382
def exponential(base: Duration, factor: Double = 2.0): Schedule[Any, Any, Duration]
383
384
/** Fibonacci sequence delays */
385
def fibonacci(one: Duration): Schedule[Any, Any, Duration]
386
387
/** Linear increasing delays */
388
def linear(base: Duration): Schedule[Any, Any, Duration]
389
390
// Conditional schedules
391
/** Continue while predicate is true */
392
def recurWhile[A](f: A => Boolean): Schedule[Any, A, A]
393
394
/** Continue until predicate is true */
395
def recurUntil[A](f: A => Boolean): Schedule[Any, A, A]
396
397
/** Collect inputs while predicate is true */
398
def collectWhile[A](f: A => Boolean): Schedule[Any, A, Chunk[A]]
399
400
/** Collect inputs until predicate is true */
401
def collectUntil[A](f: A => Boolean): Schedule[Any, A, Chunk[A]]
402
```
403
404
**Usage Examples:**
405
406
```scala
407
// Common retry patterns
408
val httpRetry = httpRequest.retry(
409
Schedule.exponential(100.millis) && Schedule.recurs(3)
410
)
411
412
val databaseRetry = databaseQuery.retry(
413
Schedule.fibonacci(1.second) && Schedule.recurs(5)
414
)
415
416
val periodicTask = taskExecution.repeat(
417
Schedule.fixed(30.minutes)
418
)
419
420
// Smart conditional retry
421
val apiRetry = apiCall.retry(
422
Schedule.recurWhile[HttpError] {
423
case HttpError(code) if code >= 500 => true // Server errors
424
case HttpError(429) => true // Rate limited
425
case _ => false // Client errors
426
} && Schedule.exponential(1.second).jittered && Schedule.recurs(3)
427
)
428
```
429
430
### Schedule Transformation and Customization
431
432
Transform and customize schedules for specific use cases and requirements.
433
434
```scala { .api }
435
/**
436
* Transform the output of the schedule
437
*/
438
def map[Out2](f: Out => Out2): Schedule[Env, In, Out2]
439
440
/**
441
* Transform output using a ZIO effect
442
*/
443
def mapZIO[Env1 <: Env, Out2](f: Out => URIO[Env1, Out2]): Schedule[Env1, In, Out2]
444
445
/**
446
* Transform the input to the schedule
447
*/
448
def contramap[In2](f: In2 => In): Schedule[Env, In2, Out]
449
450
/**
451
* Add delay to each schedule decision
452
*/
453
def addDelay(f: Out => Duration): Schedule[Env, In, Out]
454
455
/**
456
* Modify delays in the schedule
457
*/
458
def delayed(f: Duration => Duration): Schedule[Env, In, Out]
459
460
/**
461
* Add random jitter to delays
462
*/
463
def jittered: Schedule[Env, In, Out]
464
465
/**
466
* Add custom jitter
467
*/
468
def jittered(min: Double, max: Double): Schedule[Env, In, Out]
469
470
/**
471
* Continue while input satisfies predicate
472
*/
473
def whileInput[In1 <: In](f: In1 => Boolean): Schedule[Env, In1, Out]
474
475
/**
476
* Continue while output satisfies predicate
477
*/
478
def whileOutput(f: Out => Boolean): Schedule[Env, In, Out]
479
480
/**
481
* Continue until input satisfies predicate
482
*/
483
def untilInput[In1 <: In](f: In1 => Boolean): Schedule[Env, In1, Out]
484
485
/**
486
* Continue until output satisfies predicate
487
*/
488
def untilOutput(f: Out => Boolean): Schedule[Env, In, Out]
489
490
/**
491
* Collect all outputs
492
*/
493
def collectAll: Schedule[Env, In, Chunk[Out]]
494
495
/**
496
* Fold over outputs with an accumulator
497
*/
498
def fold[Z](z: Z)(f: (Z, Out) => Z): Schedule[Env, In, Z]
499
500
/**
501
* Count the number of repetitions
502
*/
503
def repetitions: Schedule[Env, In, Long]
504
```
505
506
**Usage Examples:**
507
508
```scala
509
// Custom schedule transformations
510
val adaptiveRetry = Schedule.exponential(100.millis)
511
.whileOutput(_ < 30.seconds) // Cap maximum delay
512
.jittered(0.1, 0.2) // 10-20% jitter
513
.repetitions // Track attempt count
514
515
// Conditional scheduling with state
516
val circuitBreakerSchedule = Schedule.recurs(3)
517
.whileInput[ServiceError] {
518
case ServiceUnavailable => true
519
case CircuitOpen => false
520
case _ => true
521
}
522
523
// Accumulating retry information
524
val retryWithLogging = Schedule.exponential(1.second)
525
.fold(List.empty[Attempt]) { (attempts, delay) =>
526
Attempt(System.currentTimeMillis(), delay) :: attempts
527
}
528
529
// Rate limiting schedule
530
val rateLimitedSchedule = Schedule.fixed(100.millis)
531
.whileOutput(_ => !rateLimitExceeded())
532
.mapZIO(_ => checkRateLimit())
533
```