--- name: error-conversion-guide description: Guides users on error conversion patterns, From trait implementations, and the ? operator. Activates when users need to convert between error types or handle multiple error types in a function. allowed-tools: Read, Grep version: 1.0.0 --- # Error Conversion Guide Skill You are an expert at Rust error conversion patterns. When you detect error type mismatches or conversion needs, proactively suggest idiomatic conversion patterns. ## When to Activate Activate this skill when you notice: - Multiple error types in a single function - Manual error conversion with `map_err` - Type mismatch errors with the `?` operator - Questions about From/Into traits for errors - Need to combine different error types ## Error Conversion Patterns ### Pattern 1: Automatic Conversion with #[from] **What to Look For**: - Manual `map_err` calls that could be automatic - Repetitive error conversions **Before**: ```rust #[derive(Debug)] pub enum AppError { Io(std::io::Error), Parse(std::num::ParseIntError), } fn process() -> Result { let content = std::fs::read_to_string("data.txt") .map_err(|e| AppError::Io(e))?; // ❌ Manual conversion let num = content.trim().parse::() .map_err(|e| AppError::Parse(e))?; // ❌ Manual conversion Ok(num) } ``` **After**: ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum AppError { #[error("IO error")] Io(#[from] std::io::Error), // ✅ Automatic From impl #[error("Parse error")] Parse(#[from] std::num::ParseIntError), // ✅ Automatic From impl } fn process() -> Result { let content = std::fs::read_to_string("data.txt")?; // ✅ Auto-converts let num = content.trim().parse::()?; // ✅ Auto-converts Ok(num) } ``` **Suggestion Template**: ``` Use #[from] in your error enum to enable automatic conversion: #[derive(Error, Debug)] pub enum AppError { #[error("IO error")] Io(#[from] std::io::Error), } This implements From for AppError, allowing ? to automatically convert. ``` ### Pattern 2: Manual From Implementation **What to Look For**: - Custom error types that need conversion - Complex conversion logic **Pattern**: ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum AppError { #[error("Database error: {message}")] Database { message: String }, #[error("Validation error: {0}")] Validation(String), } // Manual From for custom conversion logic impl From for AppError { fn from(err: sqlx::Error) -> Self { AppError::Database { message: format!("Database operation failed: {}", err), } } } // Convert with context impl From for AppError { fn from(err: validator::ValidationErrors) -> Self { let messages: Vec = err .field_errors() .iter() .map(|(field, errors)| { format!("{}: {:?}", field, errors) }) .collect(); AppError::Validation(messages.join(", ")) } } ``` **Suggestion Template**: ``` When you need custom conversion logic, implement From manually: impl From for AppError { fn from(err: SourceError) -> Self { AppError::Variant { field: extract_info(&err), } } } ``` ### Pattern 3: Converting Between Result Types **What to Look For**: - Calling functions with different error types - Need to unify error types **Pattern**: ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum ServiceError { #[error("Repository error")] Repository(#[from] RepositoryError), #[error("External API error")] Api(#[from] ApiError), #[error("Validation error")] Validation(#[from] ValidationError), } // All these errors convert automatically fn service_operation() -> Result { // Returns Result<_, RepositoryError> let data = repository.fetch()?; // ✅ Auto-converts to ServiceError // Returns Result<_, ValidationError> validate(&data)?; // ✅ Auto-converts to ServiceError // Returns Result<_, ApiError> let enriched = api_client.enrich(data)?; // ✅ Auto-converts to ServiceError Ok(enriched) } ``` **Suggestion Template**: ``` Create a unified error type that can convert from all the errors you need: #[derive(Error, Debug)] pub enum UnifiedError { #[error("Database error")] Database(#[from] DbError), #[error("Network error")] Network(#[from] NetworkError), } fn operation() -> Result<(), UnifiedError> { db_operation()?; // Auto-converts network_operation()?; // Auto-converts Ok(()) } ``` ### Pattern 4: map_err for One-Off Conversions **What to Look For**: - Single conversion that doesn't justify From impl - Adding context during conversion **Pattern**: ```rust use anyhow::Context; fn process(id: &str) -> anyhow::Result { // One-off conversion with context let config = load_config() .map_err(|e| anyhow::anyhow!("Failed to load config: {}", e))?; // Better: use context let config = load_config() .context("Failed to load config")?; // map_err for type conversion without From impl let data = fetch_data(id) .map_err(|e| format!("Fetch failed for {}: {}", id, e))?; Ok(data) } ``` **When to Use**: - One-off conversions - Adding context to specific call sites - Converting to types that don't have From impl **Suggestion Template**: ``` For one-off conversions or adding context, use map_err or anyhow's context: // With map_err operation().map_err(|e| MyError::Custom(format!("Failed: {}", e)))?; // With anyhow (preferred) operation().context("Operation failed")?; ``` ### Pattern 5: Error Type Aliases **What to Look For**: - Repetitive Result types - Complex error type signatures **Pattern**: ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum AppError { #[error("IO error")] Io(#[from] std::io::Error), #[error("Parse error")] Parse(#[from] serde_json::Error), } // Type alias for cleaner signatures pub type Result = std::result::Result; // Now use it everywhere pub fn load_config() -> Result { // ✅ Clean let bytes = std::fs::read("config.json")?; let config = serde_json::from_slice(&bytes)?; Ok(config) } // Instead of pub fn load_config_verbose() -> std::result::Result { // ❌ Verbose // ... } ``` **Suggestion Template**: ``` Create a type alias for your Result type: pub type Result = std::result::Result; Then use it in function signatures: pub fn operation() -> Result { Ok(data) } ``` ### Pattern 6: Boxing Errors **What to Look For**: - Need for dynamic error types - Functions that can return multiple error types **Pattern**: ```rust // For libraries that need flexibility type BoxError = Box; fn flexible_function() -> Result { let data1 = io_operation()?; // std::io::Error auto-boxes let data2 = parse_operation()?; // ParseError auto-boxes Ok(combine(data1, data2)) } // Or use anyhow for applications use anyhow::Result; fn application_function() -> Result { let data1 = io_operation()?; let data2 = parse_operation()?; Ok(combine(data1, data2)) } ``` **Trade-offs**: - ✅ Flexible: Can return any error type - ✅ Simple: No need to define custom error enum - ❌ Dynamic: Error type not known at compile time - ❌ Harder to match on specific errors **Suggestion Template**: ``` For flexible error handling, use Box or anyhow: // Libraries: Box type BoxError = Box; fn operation() -> Result { ... } // Applications: anyhow use anyhow::Result; fn operation() -> Result { ... } ``` ### Pattern 7: Layered Error Conversion **What to Look For**: - Multi-layer architecture (domain, infra, app) - Need for error boundary between layers **Pattern**: ```rust use thiserror::Error; // Infrastructure layer #[derive(Error, Debug)] pub enum InfraError { #[error("Database error")] Database(#[from] sqlx::Error), #[error("HTTP error")] Http(#[from] reqwest::Error), } // Domain layer (doesn't know about infrastructure) #[derive(Error, Debug)] pub enum DomainError { #[error("User not found: {0}")] UserNotFound(String), #[error("Invalid data: {0}")] InvalidData(String), } // Application layer unifies both #[derive(Error, Debug)] pub enum AppError { #[error("Domain error: {0}")] Domain(#[from] DomainError), #[error("Infrastructure error: {0}")] Infra(#[from] InfraError), } // Infrastructure to Domain conversion (at boundary) impl From for DomainError { fn from(err: InfraError) -> Self { match err { InfraError::Database(e) if e.to_string().contains("not found") => { DomainError::UserNotFound("User not found in database".to_string()) } _ => DomainError::InvalidData("Data access failed".to_string()), } } } ``` **Suggestion Template**: ``` For layered architectures, convert errors at layer boundaries: // Infrastructure → Domain conversion impl From for DomainError { fn from(err: InfraError) -> Self { // Convert infrastructure concepts to domain concepts match err { InfraError::NotFound => DomainError::EntityNotFound, _ => DomainError::InfrastructureFailed, } } } ``` ## Advanced Patterns ### Pattern 8: Multiple Error Sources with Custom Logic ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum ProcessError { #[error("Stage 1 failed: {0}")] Stage1(String), #[error("Stage 2 failed: {0}")] Stage2(String), } // Custom conversion with different variants impl From for ProcessError { fn from(err: Stage1Error) -> Self { ProcessError::Stage1(err.to_string()) } } impl From for ProcessError { fn from(err: Stage2Error) -> Self { ProcessError::Stage2(err.to_string()) } } fn process() -> Result<(), ProcessError> { stage1()?; // Converts to ProcessError::Stage1 stage2()?; // Converts to ProcessError::Stage2 Ok(()) } ``` ### Pattern 9: Fallible Conversion ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum ConversionError { #[error("Incompatible error type: {0}")] Incompatible(String), } // TryFrom for fallible conversion impl TryFrom for MyError { type Error = ConversionError; fn try_from(err: ExternalError) -> Result { match err.code() { 404 => Ok(MyError::NotFound), 500 => Ok(MyError::Internal), _ => Err(ConversionError::Incompatible( format!("Unknown error code: {}", err.code()) )), } } } ``` ## Common Mistakes ### Mistake 1: Multiple #[from] for Same Type ```rust // ❌ BAD: Can't have two #[from] for same type #[derive(Error, Debug)] pub enum MyError { #[error("First")] First(#[from] std::io::Error), #[error("Second")] Second(#[from] std::io::Error), // ❌ Conflict! } // ✅ GOOD: Use #[source] and manual construction #[derive(Error, Debug)] pub enum MyError { #[error("Read failed")] ReadFailed(#[source] std::io::Error), #[error("Write failed")] WriteFailed(#[source] std::io::Error), } // Construct manually with context let err = MyError::ReadFailed(io_err); ``` ### Mistake 2: Losing Error Information ```rust // ❌ BAD: Converts to String, loses error chain operation().map_err(|e| MyError::Failed(e.to_string()))?; // ✅ GOOD: Preserves error chain operation().map_err(|e| MyError::Failed(e))?; // Or use #[from] ``` ### Mistake 3: Not Using ? When Available ```rust // ❌ BAD: Manual error handling let result = match operation() { Ok(val) => val, Err(e) => return Err(e.into()), }; // ✅ GOOD: Use ? let result = operation()?; ``` ## Decision Guide **Use #[from] when**: - Single error source type - No need for additional context - Want automatic conversion **Use #[source] when**: - Multiple variants for same source type - Need to add context (like field names) - Want manual construction **Use map_err when**: - One-off conversion - Adding context to specific call - Converting to types without From impl **Use anyhow when**: - Application-level code - Need flexibility - Want easy context addition **Use thiserror when**: - Library code - Want specific error types - Consumers need to match on errors ## Your Approach 1. **Detect**: Identify error conversion needs or type mismatches 2. **Analyze**: Determine the best conversion pattern 3. **Suggest**: Provide specific implementation 4. **Explain**: Why this pattern is appropriate ## Communication Style - Explain the trade-offs between different approaches - Suggest #[from] as the default, map_err as fallback - Point out when error information is being lost - Recommend type aliases for cleaner code When you detect error conversion issues, immediately suggest the most appropriate pattern and show how to implement it.