Initial commit
This commit is contained in:
558
skills/error-conversion-guide/SKILL.md
Normal file
558
skills/error-conversion-guide/SKILL.md
Normal file
@@ -0,0 +1,558 @@
|
||||
---
|
||||
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<i32, AppError> {
|
||||
let content = std::fs::read_to_string("data.txt")
|
||||
.map_err(|e| AppError::Io(e))?; // ❌ Manual conversion
|
||||
|
||||
let num = content.trim().parse::<i32>()
|
||||
.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<i32, AppError> {
|
||||
let content = std::fs::read_to_string("data.txt")?; // ✅ Auto-converts
|
||||
let num = content.trim().parse::<i32>()?; // ✅ 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<std::io::Error> 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<sqlx::Error> for AppError {
|
||||
fn from(err: sqlx::Error) -> Self {
|
||||
AppError::Database {
|
||||
message: format!("Database operation failed: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert with context
|
||||
impl From<validator::ValidationErrors> for AppError {
|
||||
fn from(err: validator::ValidationErrors) -> Self {
|
||||
let messages: Vec<String> = 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<SourceError> 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<Data, ServiceError> {
|
||||
// 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<Data> {
|
||||
// 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<T, MyError> 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<T> = std::result::Result<T, AppError>;
|
||||
|
||||
// Now use it everywhere
|
||||
pub fn load_config() -> Result<Config> { // ✅ 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<Config, AppError> { // ❌ Verbose
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Suggestion Template**:
|
||||
```
|
||||
Create a type alias for your Result type:
|
||||
|
||||
pub type Result<T> = std::result::Result<T, AppError>;
|
||||
|
||||
Then use it in function signatures:
|
||||
|
||||
pub fn operation() -> Result<Data> {
|
||||
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<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
fn flexible_function() -> Result<Data, BoxError> {
|
||||
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<Data> {
|
||||
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<dyn Error> or anyhow:
|
||||
|
||||
// Libraries: Box<dyn Error>
|
||||
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||
fn operation() -> Result<T, BoxError> { ... }
|
||||
|
||||
// Applications: anyhow
|
||||
use anyhow::Result;
|
||||
fn operation() -> Result<T> { ... }
|
||||
```
|
||||
|
||||
### 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<InfraError> 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<InfraError> 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<Stage1Error> for ProcessError {
|
||||
fn from(err: Stage1Error) -> Self {
|
||||
ProcessError::Stage1(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Stage2Error> 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<ExternalError> for MyError {
|
||||
type Error = ConversionError;
|
||||
|
||||
fn try_from(err: ExternalError) -> Result<Self, Self::Error> {
|
||||
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.
|
||||
Reference in New Issue
Block a user