Skip to content

ADR-011: Error Handling with thiserror

Accepted

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.

RLM-RS uses thiserror for deriving error types with the following characteristics:

  1. Hierarchical error types: Top-level Error enum with domain-specific variants (StorageError, IoError, ChunkingError, CommandError)
  2. No panics in library code: All fallible operations return Result<T, Error>
  3. Source chain preservation: Errors wrap underlying causes for debugging
  4. Structured error output: JSON format includes error type and recovery suggestions
  1. No-panic guarantee: Library code must never panic, enabling safe integration
  2. Error traceability: Preserve full error chain for debugging
  3. API ergonomics: Clean error types that work with ? operator
  4. AI integration: Structured errors enable programmatic error handling
  1. Compile-time safety: Exhaustive matching on error variants
  2. Minimal boilerplate: Derive macros reduce manual implementation
  3. Standard compatibility: Implement std::error::Error trait

Derive macro for implementing std::error::Error with minimal boilerplate.

Pros:

  • Zero runtime cost
  • Automatic Display, Error, and From implementations
  • Source chain preservation via #[source]
  • Works seamlessly with ? operator

Cons:

  • Procedural macro adds compile time
  • Less flexible than manual implementation

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

Hand-written Error trait implementations.

Pros:

  • Full control over implementation details
  • No macro dependencies

Cons:

  • Significant boilerplate
  • Error-prone manual implementations
  • Harder to maintain

We chose thiserror for error handling with the following patterns:

#[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),
}

Library code uses these patterns instead of panicking:

Panic PatternSafe Alternative
.unwrap().ok_or(Error::...)?
.expect().ok_or_else(|| Error::...)?
panic!()return Err(Error::...)
unreachable!()Match all variants or use _
{
"success": false,
"error": {
"type": "BufferNotFound",
"message": "storage error: buffer not found: main",
"suggestion": "Run 'rlm-rs list' to see available buffers"
}
}
  1. Predictable behavior: No unexpected panics in production
  2. Better debugging: Full error chain available
  3. Type safety: Exhaustive matching catches missing error handlers
  4. AI-friendly: Structured errors enable automated recovery
  1. Verbose match arms: Must handle all error variants
  2. Conversion boilerplate: Manual From implementations for some types
  1. Slightly longer compile times: Procedural macros add overhead
  2. Learning curve: Contributors must understand error propagation
  1. Add variant to appropriate error enum
  2. Implement Display message via #[error("...")]
  3. Add #[from] if auto-conversion from source is desired
  4. Update get_error_details() in output.rs for JSON formatting
#[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 { .. }))));
}