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, 领域错误, 错误分类, 恢复策略, 重试, 熔断器, 优雅降级"4user-invocable: false5---67# Domain Error Strategy89> **Layer 2: Design Choices**1011## Core Question1213**Who needs to handle this error, and how should they recover?**1415Before designing error types:16- Is this user-facing or internal?17- Is recovery possible?18- What context is needed for debugging?1920---2122## Error Categorization2324| Error Type | Audience | Recovery | Example |25|------------|----------|----------|---------|26| User-facing | End users | Guide action | `InvalidEmail`, `NotFound` |27| Internal | Developers | Debug info | `DatabaseError`, `ParseError` |28| System | Ops/SRE | Monitor/alert | `ConnectionTimeout`, `RateLimited` |29| Transient | Automation | Retry | `NetworkError`, `ServiceUnavailable` |30| Permanent | Human | Investigate | `ConfigInvalid`, `DataCorrupted` |3132---3334## Thinking Prompt3536Before designing error types:37381. **Who sees this error?**39 - End user → friendly message, actionable40 - Developer → detailed, debuggable41 - Ops → structured, alertable42432. **Can we recover?**44 - Transient → retry with backoff45 - Degradable → fallback value46 - Permanent → fail fast, alert47483. **What context is needed?**49 - Call chain → anyhow::Context50 - Request ID → structured logging51 - Input data → error payload5253---5455## Trace Up ↑5657To domain constraints (Layer 3):5859```60"How should I handle payment failures?"61 ↑ Ask: What are the business rules for retries?62 ↑ Check: domain-fintech (transaction requirements)63 ↑ Check: SLA (availability requirements)64```6566| Question | Trace To | Ask |67|----------|----------|-----|68| Retry policy | domain-* | What's acceptable latency for retry? |69| User experience | domain-* | What message should users see? |70| Compliance | domain-* | What must be logged for audit? |7172---7374## Trace Down ↓7576To implementation (Layer 1):7778```79"Need typed errors"80 ↓ m06-error-handling: thiserror for library81 ↓ m04-zero-cost: Error enum design8283"Need error context"84 ↓ m06-error-handling: anyhow::Context85 ↓ Logging: tracing with fields8687"Need retry logic"88 ↓ m07-concurrency: async retry patterns89 ↓ Crates: tokio-retry, backoff90```9192---9394## Quick Reference9596| Recovery Pattern | When | Implementation |97|------------------|------|----------------|98| Retry | Transient failures | exponential backoff |99| Fallback | Degraded mode | cached/default value |100| Circuit Breaker | Cascading failures | failsafe-rs |101| Timeout | Slow operations | `tokio::time::timeout` |102| Bulkhead | Isolation | separate thread pools |103104## Error Hierarchy105106```rust107#[derive(thiserror::Error, Debug)]108pub enum AppError {109 // User-facing110 #[error("Invalid input: {0}")]111 Validation(String),112113 // Transient (retryable)114 #[error("Service temporarily unavailable")]115 ServiceUnavailable(#[source] reqwest::Error),116117 // Internal (log details, show generic)118 #[error("Internal error")]119 Internal(#[source] anyhow::Error),120}121122impl AppError {123 pub fn is_retryable(&self) -> bool {124 matches!(self, Self::ServiceUnavailable(_))125 }126}127```128129## Retry Pattern130131```rust132use tokio_retry::{Retry, strategy::ExponentialBackoff};133134async fn with_retry<F, T, E>(f: F) -> Result<T, E>135where136 F: Fn() -> impl Future<Output = Result<T, E>>,137 E: std::fmt::Debug,138{139 let strategy = ExponentialBackoff::from_millis(100)140 .max_delay(Duration::from_secs(10))141 .take(5);142143 Retry::spawn(strategy, || f()).await144}145```146147---148149## Common Mistakes150151| Mistake | Why Wrong | Better |152|---------|-----------|--------|153| Same error for all | No actionability | Categorize by audience |154| Retry everything | Wasted resources | Only transient errors |155| Infinite retry | DoS self | Max attempts + backoff |156| Expose internal errors | Security risk | User-friendly messages |157| No context | Hard to debug | .context() everywhere |158159---160161## Anti-Patterns162163| Anti-Pattern | Why Bad | Better |164|--------------|---------|--------|165| String errors | No structure | thiserror types |166| panic! for recoverable | Bad UX | Result with context |167| Ignore errors | Silent failures | Log or propagate |168| Box<dyn Error> everywhere | Lost type info | thiserror |169| Error in happy path | Performance | Early validation |170171---172173## Related Skills174175| When | See |176|------|-----|177| Error handling basics | m06-error-handling |178| Retry implementation | m07-concurrency |179| Domain modeling | m09-domain |180| User-facing APIs | domain-* |