Domain Error Strategy
Layer 2: Design Choices
Core Question
Who needs to handle this error, and how should they recover?
Before designing error types:
- Is this user-facing or internal?
- Is recovery possible?
- What context is needed for debugging?
Error Categorization
| Error Type |
Audience |
Recovery |
Example |
| User-facing |
End users |
Guide action |
InvalidEmail, NotFound |
| Internal |
Developers |
Debug info |
DatabaseError, ParseError |
| System |
Ops/SRE |
Monitor/alert |
ConnectionTimeout, RateLimited |
| Transient |
Automation |
Retry |
NetworkError, ServiceUnavailable |
| Permanent |
Human |
Investigate |
ConfigInvalid, DataCorrupted |
Thinking Prompt
Before designing error types:
Who sees this error?
- End user → friendly message, actionable
- Developer → detailed, debuggable
- Ops → structured, alertable
Can we recover?
- Transient → retry with backoff
- Degradable → fallback value
- Permanent → fail fast, alert
What context is needed?
- Call chain → anyhow::Context
- Request ID → structured logging
- Input data → error payload
Trace Up ↑
To domain constraints (Layer 3):
"How should I handle payment failures?"
↑ Ask: What are the business rules for retries?
↑ Check: domain-fintech (transaction requirements)
↑ Check: SLA (availability requirements)
| Question |
Trace To |
Ask |
| Retry policy |
domain-* |
What's acceptable latency for retry? |
| User experience |
domain-* |
What message should users see? |
| Compliance |
domain-* |
What must be logged for audit? |
Trace Down ↓
To implementation (Layer 1):
"Need typed errors"
↓ m06-error-handling: thiserror for library
↓ m04-zero-cost: Error enum design
"Need error context"
↓ m06-error-handling: anyhow::Context
↓ Logging: tracing with fields
"Need retry logic"
↓ m07-concurrency: async retry patterns
↓ Crates: tokio-retry, backoff
Quick Reference
| Recovery Pattern |
When |
Implementation |
| Retry |
Transient failures |
exponential backoff |
| Fallback |
Degraded mode |
cached/default value |
| Circuit Breaker |
Cascading failures |
failsafe-rs |
| Timeout |
Slow operations |
tokio::time::timeout |
| Bulkhead |
Isolation |
separate thread pools |
Error Hierarchy
#[derive(thiserror::Error, Debug)]
pub enum AppError {
// User-facing
#[error("Invalid input: {0}")]
Validation(String),
// Transient (retryable)
#[error("Service temporarily unavailable")]
ServiceUnavailable(#[source] reqwest::Error),
// Internal (log details, show generic)
#[error("Internal error")]
Internal(#[source] anyhow::Error),
}
impl AppError {
pub fn is_retryable(&self) -> bool {
matches!(self, Self::ServiceUnavailable(_))
}
}
Retry Pattern
use tokio_retry::{Retry, strategy::ExponentialBackoff};
async fn with_retry<F, T, E>(f: F) -> Result<T, E>
where
F: Fn() -> impl Future<Output = Result<T, E>>,
E: std::fmt::Debug,
{
let strategy = ExponentialBackoff::from_millis(100)
.max_delay(Duration::from_secs(10))
.take(5);
Retry::spawn(strategy, || f()).await
}
Common Mistakes
| Mistake |
Why Wrong |
Better |
| Same error for all |
No actionability |
Categorize by audience |
| Retry everything |
Wasted resources |
Only transient errors |
| Infinite retry |
DoS self |
Max attempts + backoff |
| Expose internal errors |
Security risk |
User-friendly messages |
| No context |
Hard to debug |
.context() everywhere |
Anti-Patterns
| Anti-Pattern |
Why Bad |
Better |
| String errors |
No structure |
thiserror types |
| panic! for recoverable |
Bad UX |
Result with context |
| Ignore errors |
Silent failures |
Log or propagate |
| Box everywhere |
Lost type info |
thiserror |
| Error in happy path |
Performance |
Early validation |
Related Skills
| When |
See |
| Error handling basics |
m06-error-handling |
| Retry implementation |
m07-concurrency |
| Domain modeling |
m09-domain |
| User-facing APIs |
domain-* |
1---2name: m13-domain-error3description: Use when designing domain error handling. Keywords: domain error, error categorization, recovery strategy, retry, fallback, domain error hierarchy, user-facing vs internal errors, error code design, circuit breaker, graceful degradation, resilience, error context, backoff, retry with backoff, error recovery, transient vs permanent error, 领域错误, 错误分类, 恢复策略, 重试, 熔断器, 优雅降级4---5
6# Domain Error Strategy
7
8> **Layer 2: Design Choices**
9
10## Core Question
11
12**Who needs to handle this error, and how should they recover?**
13
14Before designing error types:
15- Is this user-facing or internal?
16- Is recovery possible?
17- What context is needed for debugging?
18
19---
20
21## Error Categorization
22
23| Error Type | Audience | Recovery | Example |
24|------------|----------|----------|---------|
25| User-facing | End users | Guide action | `InvalidEmail`, `NotFound` |
26| Internal | Developers | Debug info | `DatabaseError`, `ParseError` |
27| System | Ops/SRE | Monitor/alert | `ConnectionTimeout`, `RateLimited` |
28| Transient | Automation | Retry | `NetworkError`, `ServiceUnavailable` |
29| Permanent | Human | Investigate | `ConfigInvalid`, `DataCorrupted` |
30
31---
32
33## Thinking Prompt
34
35Before designing error types:
36
371. **Who sees this error?**
38 - End user → friendly message, actionable
39 - Developer → detailed, debuggable
40 - Ops → structured, alertable
41
422. **Can we recover?**
43 - Transient → retry with backoff
44 - Degradable → fallback value
45 - Permanent → fail fast, alert
46
473. **What context is needed?**
48 - Call chain → anyhow::Context
49 - Request ID → structured logging
50 - Input data → error payload
51
52---
53
54## Trace Up ↑
55
56To domain constraints (Layer 3):
57
58```
59"How should I handle payment failures?"
60 ↑ Ask: What are the business rules for retries?
61 ↑ Check: domain-fintech (transaction requirements)
62 ↑ Check: SLA (availability requirements)
63```
64
65| Question | Trace To | Ask |
66|----------|----------|-----|
67| Retry policy | domain-* | What's acceptable latency for retry? |
68| User experience | domain-* | What message should users see? |
69| Compliance | domain-* | What must be logged for audit? |
70
71---
72
73## Trace Down ↓
74
75To implementation (Layer 1):
76
77```
78"Need typed errors"
79 ↓ m06-error-handling: thiserror for library
80 ↓ m04-zero-cost: Error enum design
81
82"Need error context"
83 ↓ m06-error-handling: anyhow::Context
84 ↓ Logging: tracing with fields
85
86"Need retry logic"
87 ↓ m07-concurrency: async retry patterns
88 ↓ Crates: tokio-retry, backoff
89```
90
91---
92
93## Quick Reference
94
95| Recovery Pattern | When | Implementation |
96|------------------|------|----------------|
97| Retry | Transient failures | exponential backoff |
98| Fallback | Degraded mode | cached/default value |
99| Circuit Breaker | Cascading failures | failsafe-rs |
100| Timeout | Slow operations | `tokio::time::timeout` |
101| Bulkhead | Isolation | separate thread pools |
102
103## Error Hierarchy
104
105```rust
106#[derive(thiserror::Error, Debug)]
107pub enum AppError {
108 // User-facing
109 #[error("Invalid input: {0}")]
110 Validation(String),
111
112 // Transient (retryable)
113 #[error("Service temporarily unavailable")]
114 ServiceUnavailable(#[source] reqwest::Error),
115
116 // Internal (log details, show generic)
117 #[error("Internal error")]
118 Internal(#[source] anyhow::Error),
119}
120
121impl AppError {
122 pub fn is_retryable(&self) -> bool {
123 matches!(self, Self::ServiceUnavailable(_))
124 }
125}
126```
127
128## Retry Pattern
129
130```rust
131use tokio_retry::{Retry, strategy::ExponentialBackoff};
132
133async fn with_retry<F, T, E>(f: F) -> Result<T, E>
134where
135 F: Fn() -> impl Future<Output = Result<T, E>>,
136 E: std::fmt::Debug,
137{
138 let strategy = ExponentialBackoff::from_millis(100)
139 .max_delay(Duration::from_secs(10))
140 .take(5);
141
142 Retry::spawn(strategy, || f()).await
143}
144```
145
146---
147
148## Common Mistakes
149
150| Mistake | Why Wrong | Better |
151|---------|-----------|--------|
152| Same error for all | No actionability | Categorize by audience |
153| Retry everything | Wasted resources | Only transient errors |
154| Infinite retry | DoS self | Max attempts + backoff |
155| Expose internal errors | Security risk | User-friendly messages |
156| No context | Hard to debug | .context() everywhere |
157
158---
159
160## Anti-Patterns
161
162| Anti-Pattern | Why Bad | Better |
163|--------------|---------|--------|
164| String errors | No structure | thiserror types |
165| panic! for recoverable | Bad UX | Result with context |
166| Ignore errors | Silent failures | Log or propagate |
167| Box<dyn Error> everywhere | Lost type info | thiserror |
168| Error in happy path | Performance | Early validation |
169
170---
171
172## Related Skills
173
174| When | See |
175|------|-----|
176| Error handling basics | m06-error-handling |
177| Retry implementation | m07-concurrency |
178| Domain modeling | m09-domain |
179| User-facing APIs | domain-* |