ADR-011: Error Handling with thiserror
ADR-011: Error Handling with thiserror
Section titled “ADR-011: Error Handling with thiserror”Status
Section titled “Status”Accepted
Context
Section titled “Context”Background and Problem Statement
Section titled “Background and Problem Statement”Error handling in Rust libraries requires careful consideration of several factors: type safety, error context preservation, API ergonomics, and panic avoidance. For a CLI tool that may be integrated into AI assistant workflows, predictable error behavior is critical. AI assistants need structured error information to provide recovery guidance.
Current Approach
Section titled “Current Approach”RLM-RS uses thiserror for deriving error types with the following characteristics:
- Hierarchical error types: Top-level
Errorenum with domain-specific variants (StorageError,IoError,ChunkingError,CommandError) - No panics in library code: All fallible operations return
Result<T, Error> - Source chain preservation: Errors wrap underlying causes for debugging
- Structured error output: JSON format includes error type and recovery suggestions
Decision Drivers
Section titled “Decision Drivers”Primary Decision Drivers
Section titled “Primary Decision Drivers”- No-panic guarantee: Library code must never panic, enabling safe integration
- Error traceability: Preserve full error chain for debugging
- API ergonomics: Clean error types that work with
?operator - AI integration: Structured errors enable programmatic error handling
Secondary Decision Drivers
Section titled “Secondary Decision Drivers”- Compile-time safety: Exhaustive matching on error variants
- Minimal boilerplate: Derive macros reduce manual implementation
- Standard compatibility: Implement
std::error::Errortrait
Considered Options
Section titled “Considered Options”Option 1: thiserror (Chosen)
Section titled “Option 1: thiserror (Chosen)”Derive macro for implementing std::error::Error with minimal boilerplate.
Pros:
- Zero runtime cost
- Automatic
Display,Error, andFromimplementations - Source chain preservation via
#[source] - Works seamlessly with
?operator
Cons:
- Procedural macro adds compile time
- Less flexible than manual implementation
Option 2: anyhow
Section titled “Option 2: anyhow”Type-erased error handling for applications.
Pros:
- Simple API with automatic context attachment
- Good for applications where error types aren’t part of API
Cons:
- Type erasure loses compile-time error variant checking
- Not suitable for library APIs where callers need to match on errors
Option 3: Manual Implementation
Section titled “Option 3: Manual Implementation”Hand-written Error trait implementations.
Pros:
- Full control over implementation details
- No macro dependencies
Cons:
- Significant boilerplate
- Error-prone manual implementations
- Harder to maintain
Decision
Section titled “Decision”We chose thiserror for error handling with the following patterns:
Error Hierarchy
Section titled “Error Hierarchy”#[derive(Error, Debug)]pub enum Error { #[error("storage error: {0}")] Storage(#[from] StorageError),
#[error("I/O error: {0}")] Io(#[from] IoError),
#[error("chunking error: {0}")] Chunking(#[from] ChunkingError),
#[error("command error: {0}")] Command(#[from] CommandError),}No-Panic Policy
Section titled “No-Panic Policy”Library code uses these patterns instead of panicking:
| Panic Pattern | Safe Alternative |
|---|---|
.unwrap() | .ok_or(Error::...)? |
.expect() | .ok_or_else(|| Error::...)? |
panic!() | return Err(Error::...) |
unreachable!() | Match all variants or use _ |
Structured JSON Errors
Section titled “Structured JSON Errors”{ "success": false, "error": { "type": "BufferNotFound", "message": "storage error: buffer not found: main", "suggestion": "Run 'rlm-rs list' to see available buffers" }}Consequences
Section titled “Consequences”Positive Consequences
Section titled “Positive Consequences”- Predictable behavior: No unexpected panics in production
- Better debugging: Full error chain available
- Type safety: Exhaustive matching catches missing error handlers
- AI-friendly: Structured errors enable automated recovery
Negative Consequences
Section titled “Negative Consequences”- Verbose match arms: Must handle all error variants
- Conversion boilerplate: Manual
Fromimplementations for some types
Neutral Consequences
Section titled “Neutral Consequences”- Slightly longer compile times: Procedural macros add overhead
- Learning curve: Contributors must understand error propagation
Implementation Notes
Section titled “Implementation Notes”Adding New Error Variants
Section titled “Adding New Error Variants”- Add variant to appropriate error enum
- Implement
Displaymessage via#[error("...")] - Add
#[from]if auto-conversion from source is desired - Update
get_error_details()inoutput.rsfor JSON formatting
Testing Error Paths
Section titled “Testing Error Paths”#[test]fn test_error_on_missing_buffer() { let storage = create_test_storage(); let result = storage.get_buffer("nonexistent"); assert!(matches!(result, Err(Error::Storage(StorageError::BufferNotFound { .. }))));}