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---56# Domain Error Strategy78> **Layer 2: Design Choices**910## Core Question1112**Who needs to handle this error, and how should they recover?**1314Before designing error types:15- Is this user-facing or internal?16- Is recovery possible?17- What context is needed for debugging?1819---2021## Error Categorization2223| 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` |3031---3233## Thinking Prompt3435Before designing error types:36371. **Who sees this error?**38 - End user → friendly message, actionable39 - Developer → detailed, debuggable40 - Ops → structured, alertable41422. **Can we recover?**43 - Transient → retry with backoff44 - Degradable → fallback value45 - Permanent → fail fast, alert46473. **What context is needed?**48 - Call chain → anyhow::Context49 - Request ID → structured logging50 - Input data → error payload5152---5354## Trace Up ↑5556To domain constraints (Layer 3):5758```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```6465| 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? |7071---7273## Trace Down ↓7475To implementation (Layer 1):7677```78"Need typed errors"79 ↓ m06-error-handling: thiserror for library80 ↓ m04-zero-cost: Error enum design8182"Need error context"83 ↓ m06-error-handling: anyhow::Context84 ↓ Logging: tracing with fields8586"Need retry logic"87 ↓ m07-concurrency: async retry patterns88 ↓ Crates: tokio-retry, backoff89```9091---9293## Quick Reference9495| 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 |102103## Error Hierarchy104105```rust106#[derive(thiserror::Error, Debug)]107pub enum AppError {108 // User-facing109 #[error("Invalid input: {0}")]110 Validation(String),111112 // Transient (retryable)113 #[error("Service temporarily unavailable")]114 ServiceUnavailable(#[source] reqwest::Error),115116 // Internal (log details, show generic)117 #[error("Internal error")]118 Internal(#[source] anyhow::Error),119}120121impl AppError {122 pub fn is_retryable(&self) -> bool {123 matches!(self, Self::ServiceUnavailable(_))124 }125}126```127128## Retry Pattern129130```rust131use tokio_retry::{Retry, strategy::ExponentialBackoff};132133async fn with_retry<F, T, E>(f: F) -> Result<T, E>134where135 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);141142 Retry::spawn(strategy, || f()).await143}144```145146---147148## Common Mistakes149150| 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 |157158---159160## Anti-Patterns161162| 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 |169170---171172## Related Skills173174| 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-* |