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.
|
||||
444
skills/error-handler-advisor/SKILL.md
Normal file
444
skills/error-handler-advisor/SKILL.md
Normal file
@@ -0,0 +1,444 @@
|
||||
---
|
||||
name: error-handler-advisor
|
||||
description: Proactively reviews error handling patterns and suggests improvements using Result types, proper error propagation, and idiomatic patterns. Activates when users write error handling code or use unwrap/expect.
|
||||
allowed-tools: Read, Grep, Glob
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Error Handler Advisor Skill
|
||||
|
||||
You are an expert at Rust error handling patterns. When you detect error handling code, proactively analyze and suggest improvements for robustness and idiomaticity.
|
||||
|
||||
## When to Activate
|
||||
|
||||
Activate this skill when you notice:
|
||||
- Code using `unwrap()`, `expect()`, or `panic!()`
|
||||
- Functions returning `Result` or `Option` types
|
||||
- Error propagation with `?` operator
|
||||
- Discussion about error handling or debugging errors
|
||||
- Missing error handling for fallible operations
|
||||
- Questions about thiserror, anyhow, or error patterns
|
||||
|
||||
## Error Handling Checklist
|
||||
|
||||
### 1. Unwrap/Expect Usage
|
||||
|
||||
**What to Look For**:
|
||||
- `unwrap()` or `expect()` in production code
|
||||
- Potential panic points
|
||||
- Missing error handling
|
||||
|
||||
**Bad Pattern**:
|
||||
```rust
|
||||
fn process_user(id: &str) -> User {
|
||||
let user = db.find_user(id).unwrap(); // ❌ Will panic if not found
|
||||
let config = load_config().expect("config must exist"); // ❌ Crashes on error
|
||||
user
|
||||
}
|
||||
```
|
||||
|
||||
**Good Pattern**:
|
||||
```rust
|
||||
fn process_user(id: &str) -> Result<User, Error> {
|
||||
let user = db.find_user(id)?; // ✅ Propagates error
|
||||
let config = load_config()
|
||||
.context("Failed to load configuration")?; // ✅ Adds context
|
||||
Ok(user)
|
||||
}
|
||||
```
|
||||
|
||||
**Suggestion Template**:
|
||||
```
|
||||
I notice you're using unwrap() which will panic if the Result is Err. Consider propagating the error instead:
|
||||
|
||||
fn process_user(id: &str) -> Result<User, Error> {
|
||||
let user = db.find_user(id)?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
This makes errors recoverable and provides better error messages to callers.
|
||||
```
|
||||
|
||||
### 2. Custom Error Types
|
||||
|
||||
**What to Look For**:
|
||||
- String as error type
|
||||
- Missing custom error enums
|
||||
- Library code without specific error types
|
||||
- No error conversion implementations
|
||||
|
||||
**Bad Pattern**:
|
||||
```rust
|
||||
fn validate_email(email: &str) -> Result<(), String> {
|
||||
if email.is_empty() {
|
||||
return Err("Email cannot be empty".to_string()); // ❌ String errors
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Good Pattern**:
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ValidationError {
|
||||
#[error("Email cannot be empty")]
|
||||
EmptyEmail,
|
||||
|
||||
#[error("Invalid email format: {0}")]
|
||||
InvalidFormat(String),
|
||||
|
||||
#[error("Email too long (max {max}, got {actual})")]
|
||||
TooLong { max: usize, actual: usize },
|
||||
}
|
||||
|
||||
fn validate_email(email: &str) -> Result<(), ValidationError> {
|
||||
if email.is_empty() {
|
||||
return Err(ValidationError::EmptyEmail); // ✅ Typed error
|
||||
}
|
||||
if !email.contains('@') {
|
||||
return Err(ValidationError::InvalidFormat(email.to_string()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Suggestion Template**:
|
||||
```
|
||||
Using String as an error type loses type information. Consider using thiserror for custom error types:
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ValidationError {
|
||||
#[error("Email cannot be empty")]
|
||||
EmptyEmail,
|
||||
|
||||
#[error("Invalid email format: {0}")]
|
||||
InvalidFormat(String),
|
||||
}
|
||||
|
||||
This provides:
|
||||
- Type safety for error handling
|
||||
- Automatic Display implementation
|
||||
- Better error matching
|
||||
- Clear error semantics
|
||||
```
|
||||
|
||||
### 3. Error Propagation
|
||||
|
||||
**What to Look For**:
|
||||
- Manual error handling that could use `?`
|
||||
- Nested match statements for Result
|
||||
- Losing error context during propagation
|
||||
|
||||
**Bad Pattern**:
|
||||
```rust
|
||||
fn process() -> Result<Data, Error> {
|
||||
let config = match load_config() {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(e), // ❌ Verbose
|
||||
};
|
||||
|
||||
let data = match fetch_data(&config) {
|
||||
Ok(d) => d,
|
||||
Err(e) => return Err(e), // ❌ Repetitive
|
||||
};
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
```
|
||||
|
||||
**Good Pattern**:
|
||||
```rust
|
||||
fn process() -> Result<Data, Error> {
|
||||
let config = load_config()?; // ✅ Concise
|
||||
let data = fetch_data(&config)?; // ✅ Clear
|
||||
Ok(data)
|
||||
}
|
||||
```
|
||||
|
||||
**Suggestion Template**:
|
||||
```
|
||||
You can simplify error propagation using the ? operator:
|
||||
|
||||
fn process() -> Result<Data, Error> {
|
||||
let config = load_config()?;
|
||||
let data = fetch_data(&config)?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
The ? operator automatically propagates errors up the call stack.
|
||||
```
|
||||
|
||||
### 4. Error Context
|
||||
|
||||
**What to Look For**:
|
||||
- Errors without context about what operation failed
|
||||
- Missing information for debugging
|
||||
- Bare error propagation
|
||||
|
||||
**Bad Pattern**:
|
||||
```rust
|
||||
fn load_user_data(id: &str) -> Result<UserData, Error> {
|
||||
let user = fetch_user(id)?; // ❌ No context
|
||||
let profile = fetch_profile(id)?; // ❌ Which operation failed?
|
||||
Ok(UserData { user, profile })
|
||||
}
|
||||
```
|
||||
|
||||
**Good Pattern**:
|
||||
```rust
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
fn load_user_data(id: &str) -> Result<UserData> {
|
||||
let user = fetch_user(id)
|
||||
.context(format!("Failed to fetch user {}", id))?; // ✅ Context added
|
||||
|
||||
let profile = fetch_profile(id)
|
||||
.context(format!("Failed to fetch profile for user {}", id))?; // ✅ Clear context
|
||||
|
||||
Ok(UserData { user, profile })
|
||||
}
|
||||
```
|
||||
|
||||
**Suggestion Template**:
|
||||
```
|
||||
Add context to errors to make debugging easier:
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
let user = fetch_user(id)
|
||||
.context(format!("Failed to fetch user {}", id))?;
|
||||
|
||||
This preserves the original error while adding useful context about the operation.
|
||||
```
|
||||
|
||||
### 5. Error Conversion
|
||||
|
||||
**What to Look For**:
|
||||
- Missing From implementations
|
||||
- Manual error conversion
|
||||
- Incompatible error types
|
||||
|
||||
**Bad Pattern**:
|
||||
```rust
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
#[error("Database error")]
|
||||
Database(String), // ❌ Loses original error
|
||||
}
|
||||
|
||||
fn query_db() -> Result<Data, AppError> {
|
||||
let result = sqlx::query("SELECT ...").fetch_one(&pool).await
|
||||
.map_err(|e| AppError::Database(e.to_string()))?; // ❌ Manual conversion, loses details
|
||||
Ok(result)
|
||||
}
|
||||
```
|
||||
|
||||
**Good Pattern**:
|
||||
```rust
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
#[error("Database error")]
|
||||
Database(#[from] sqlx::Error), // ✅ Automatic conversion, preserves error
|
||||
}
|
||||
|
||||
fn query_db() -> Result<Data, AppError> {
|
||||
let result = sqlx::query("SELECT ...").fetch_one(&pool).await?; // ✅ Auto-converts
|
||||
Ok(result)
|
||||
}
|
||||
```
|
||||
|
||||
**Suggestion Template**:
|
||||
```
|
||||
Use the #[from] attribute for automatic error conversion:
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
#[error("Database error")]
|
||||
Database(#[from] sqlx::Error),
|
||||
}
|
||||
|
||||
This implements From<sqlx::Error> for AppError automatically, allowing ? to convert errors.
|
||||
```
|
||||
|
||||
### 6. Library vs Application Errors
|
||||
|
||||
**What to Look For**:
|
||||
- Library code using anyhow
|
||||
- Application code with overly specific error types
|
||||
- Missing error type patterns
|
||||
|
||||
**Library Code Pattern**:
|
||||
```rust
|
||||
// ✅ Libraries should use thiserror with specific error types
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ParseError {
|
||||
#[error("Invalid syntax at line {line}: {msg}")]
|
||||
Syntax { line: usize, msg: String },
|
||||
|
||||
#[error("IO error")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
pub fn parse_file(path: &Path) -> Result<Ast, ParseError> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
parse_content(&content)
|
||||
}
|
||||
```
|
||||
|
||||
**Application Code Pattern**:
|
||||
```rust
|
||||
// ✅ Applications can use anyhow for flexibility
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let config = load_config()
|
||||
.context("Failed to load config.toml")?;
|
||||
|
||||
let app = initialize_app(&config)
|
||||
.context("Failed to initialize application")?;
|
||||
|
||||
app.run()
|
||||
.context("Application failed during execution")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Suggestion Template**:
|
||||
```
|
||||
For library code, use thiserror with specific error types:
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MyLibError {
|
||||
#[error("Specific error: {0}")]
|
||||
Specific(String),
|
||||
}
|
||||
|
||||
For application code, anyhow provides flexibility:
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
operation().context("Operation failed")?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Common Anti-Patterns
|
||||
|
||||
### Anti-Pattern 1: Ignoring Errors
|
||||
|
||||
**Bad**:
|
||||
```rust
|
||||
let _ = dangerous_operation(); // ❌ Silently ignores errors
|
||||
```
|
||||
|
||||
**Good**:
|
||||
```rust
|
||||
dangerous_operation()?; // ✅ Propagates error
|
||||
// or
|
||||
if let Err(e) = dangerous_operation() {
|
||||
warn!("Operation failed: {}", e); // ✅ At least log it
|
||||
}
|
||||
```
|
||||
|
||||
### Anti-Pattern 2: Too Generic Errors
|
||||
|
||||
**Bad**:
|
||||
```rust
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Something went wrong: {0}")]
|
||||
Generic(String), // ❌ Not specific enough
|
||||
}
|
||||
```
|
||||
|
||||
**Good**:
|
||||
```rust
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("User not found: {0}")]
|
||||
UserNotFound(String),
|
||||
|
||||
#[error("Invalid credentials")]
|
||||
InvalidCredentials,
|
||||
|
||||
#[error("Database connection failed")]
|
||||
DatabaseConnection, // ✅ Specific variants
|
||||
}
|
||||
```
|
||||
|
||||
### Anti-Pattern 3: Panicking in Libraries
|
||||
|
||||
**Bad**:
|
||||
```rust
|
||||
pub fn parse(input: &str) -> Value {
|
||||
if input.is_empty() {
|
||||
panic!("Input cannot be empty"); // ❌ Library shouldn't panic
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Good**:
|
||||
```rust
|
||||
pub fn parse(input: &str) -> Result<Value, ParseError> {
|
||||
if input.is_empty() {
|
||||
return Err(ParseError::EmptyInput); // ✅ Return error
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Error Testing Patterns
|
||||
|
||||
### Test Error Cases
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_empty_email_error() {
|
||||
let result = validate_email("");
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), ValidationError::EmptyEmail));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_format_error() {
|
||||
let result = validate_email("invalid");
|
||||
match result {
|
||||
Err(ValidationError::InvalidFormat(email)) => {
|
||||
assert_eq!(email, "invalid");
|
||||
}
|
||||
_ => panic!("Expected InvalidFormat error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Your Approach
|
||||
|
||||
1. **Detect**: Identify error handling code or potential error cases
|
||||
2. **Analyze**: Check against the checklist above
|
||||
3. **Suggest**: Provide specific improvements with code examples
|
||||
4. **Explain**: Why the suggested pattern is better
|
||||
5. **Prioritize**: Focus on potential panics and missing error handling first
|
||||
|
||||
## Communication Style
|
||||
|
||||
- Point out potential panics immediately
|
||||
- Suggest thiserror for libraries, anyhow for applications
|
||||
- Provide complete code examples, not just fragments
|
||||
- Explain the benefits of each pattern
|
||||
- Consider the context (library vs application, production vs prototype)
|
||||
|
||||
When you detect error handling code, quickly scan for common issues and proactively suggest improvements that will make the code more robust and maintainable.
|
||||
532
skills/thiserror-expert/SKILL.md
Normal file
532
skills/thiserror-expert/SKILL.md
Normal file
@@ -0,0 +1,532 @@
|
||||
---
|
||||
name: thiserror-expert
|
||||
description: Provides guidance on creating custom error types with thiserror, including proper derive macros, error messages, and source error chaining. Activates when users define error enums or work with thiserror.
|
||||
allowed-tools: Read, Grep
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Thiserror Expert Skill
|
||||
|
||||
You are an expert at using the thiserror crate to create elegant, idiomatic Rust error types. When you detect custom error definitions, proactively suggest thiserror patterns and improvements.
|
||||
|
||||
## When to Activate
|
||||
|
||||
Activate this skill when you notice:
|
||||
- Custom error enum definitions
|
||||
- Manual Display or Error implementations
|
||||
- Code using `thiserror::Error` derive macro
|
||||
- Questions about error types or thiserror usage
|
||||
- Library code that needs custom error types
|
||||
|
||||
## Thiserror Patterns
|
||||
|
||||
### Pattern 1: Basic Error Enum
|
||||
|
||||
**What to Look For**:
|
||||
- Manual Display implementations
|
||||
- Missing thiserror derive
|
||||
|
||||
**Before**:
|
||||
```rust
|
||||
#[derive(Debug)]
|
||||
pub enum MyError {
|
||||
NotFound,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MyError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
MyError::NotFound => write!(f, "Not found"),
|
||||
MyError::Invalid => write!(f, "Invalid"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MyError {}
|
||||
```
|
||||
|
||||
**After**:
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MyError {
|
||||
#[error("Not found")]
|
||||
NotFound,
|
||||
|
||||
#[error("Invalid")]
|
||||
Invalid,
|
||||
}
|
||||
```
|
||||
|
||||
**Suggestion Template**:
|
||||
```
|
||||
You can simplify your error type using thiserror:
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MyError {
|
||||
#[error("Not found")]
|
||||
NotFound,
|
||||
|
||||
#[error("Invalid input")]
|
||||
Invalid,
|
||||
}
|
||||
|
||||
This automatically implements Display and std::error::Error.
|
||||
```
|
||||
|
||||
### Pattern 2: Error Messages with Fields
|
||||
|
||||
**What to Look For**:
|
||||
- Error variants with data
|
||||
- Need to include field values in error messages
|
||||
|
||||
**Patterns**:
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ValidationError {
|
||||
// Positional fields (tuple variants)
|
||||
#[error("Invalid email: {0}")]
|
||||
InvalidEmail(String),
|
||||
|
||||
// Named fields with standard display
|
||||
#[error("Value {value} out of range (min: {min}, max: {max})")]
|
||||
OutOfRange { value: i32, min: i32, max: i32 },
|
||||
|
||||
// Custom formatting with debug
|
||||
#[error("Invalid character: {ch:?} at position {pos}")]
|
||||
InvalidChar { ch: char, pos: usize },
|
||||
|
||||
// Multiple positional args
|
||||
#[error("Cannot convert {0} to {1}")]
|
||||
ConversionFailed(String, String),
|
||||
}
|
||||
```
|
||||
|
||||
**Suggestion Template**:
|
||||
```
|
||||
You can include field values in error messages:
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MyError {
|
||||
#[error("User {user_id} not found")]
|
||||
UserNotFound { user_id: String },
|
||||
|
||||
#[error("Invalid age: {0} (must be >= 18)")]
|
||||
InvalidAge(u32),
|
||||
}
|
||||
|
||||
Use {field} for named fields and {0}, {1} for positional fields.
|
||||
```
|
||||
|
||||
### Pattern 3: Wrapping Source Errors with #[from]
|
||||
|
||||
**What to Look For**:
|
||||
- Error variants that wrap other errors
|
||||
- Missing automatic conversions
|
||||
|
||||
**Pattern**:
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
// Automatic From implementation
|
||||
#[error("IO error")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
// Multiple source error types
|
||||
#[error("Database error")]
|
||||
Database(#[from] sqlx::Error),
|
||||
|
||||
#[error("Serialization error")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
// Application-specific errors (no #[from])
|
||||
#[error("User not found: {0}")]
|
||||
UserNotFound(String),
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Implements `From<std::io::Error> for AppError`
|
||||
- Allows `?` operator to auto-convert
|
||||
- Preserves source error for debugging
|
||||
|
||||
**Suggestion Template**:
|
||||
```
|
||||
Use #[from] to automatically implement From for error conversion:
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
#[error("IO error")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Database error")]
|
||||
Database(#[from] sqlx::Error),
|
||||
}
|
||||
|
||||
This allows the ? operator to automatically convert these errors to AppError.
|
||||
```
|
||||
|
||||
### Pattern 4: Source Error Chain with #[source]
|
||||
|
||||
**What to Look For**:
|
||||
- Errors that wrap other errors but need custom messages
|
||||
- Need for error source chain
|
||||
|
||||
**Pattern**:
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ConfigError {
|
||||
// #[source] preserves error chain without #[from]
|
||||
#[error("Failed to load config file")]
|
||||
LoadFailed(#[source] std::io::Error),
|
||||
|
||||
// #[source] with custom error info
|
||||
#[error("Invalid config format in {file}")]
|
||||
InvalidFormat {
|
||||
file: String,
|
||||
#[source]
|
||||
source: toml::de::Error,
|
||||
},
|
||||
|
||||
// Both message customization and error chain
|
||||
#[error("Missing required field: {field}")]
|
||||
MissingField {
|
||||
field: String,
|
||||
#[source]
|
||||
source: Box<dyn std::error::Error + Send + Sync>,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Difference from #[from]**:
|
||||
- `#[from]`: Implements `From` trait (automatic conversion)
|
||||
- `#[source]`: Only marks as source error (manual construction)
|
||||
|
||||
**Suggestion Template**:
|
||||
```
|
||||
Use #[source] when you need custom error construction but want to preserve the error chain:
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MyError {
|
||||
#[error("Operation failed for user {user_id}")]
|
||||
OperationFailed {
|
||||
user_id: String,
|
||||
#[source]
|
||||
source: DatabaseError,
|
||||
},
|
||||
}
|
||||
|
||||
// Construct manually with context
|
||||
return Err(MyError::OperationFailed {
|
||||
user_id: id.to_string(),
|
||||
source: db_error,
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 5: Transparent Error Forwarding
|
||||
|
||||
**What to Look For**:
|
||||
- Wrapper errors that should forward to inner error
|
||||
- Need for transparent error propagation
|
||||
|
||||
**Pattern**:
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum WrapperError {
|
||||
// Transparent forwards all Display/source to inner error
|
||||
#[error(transparent)]
|
||||
Inner(#[from] InnerError),
|
||||
}
|
||||
|
||||
// Example: Wrapper for anyhow in library
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LibError {
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases**:
|
||||
- Wrapping errors without changing their display
|
||||
- Re-exporting errors from dependencies
|
||||
- Internal error handling that shouldn't change messages
|
||||
|
||||
**Suggestion Template**:
|
||||
```
|
||||
Use #[error(transparent)] to forward all error information to the inner error:
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MyError {
|
||||
#[error(transparent)]
|
||||
Wrapped(#[from] InnerError),
|
||||
}
|
||||
|
||||
This preserves the inner error's Display and source chain completely.
|
||||
```
|
||||
|
||||
### Pattern 6: Layered Errors
|
||||
|
||||
**What to Look For**:
|
||||
- Applications with multiple layers (domain, infrastructure, etc.)
|
||||
- Need for error conversion between layers
|
||||
|
||||
**Pattern**:
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
// Domain layer errors
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DomainError {
|
||||
#[error("Invalid user data: {0}")]
|
||||
InvalidUser(String),
|
||||
|
||||
#[error("Business rule violated: {0}")]
|
||||
BusinessRuleViolation(String),
|
||||
}
|
||||
|
||||
// Infrastructure layer errors
|
||||
#[derive(Error, Debug)]
|
||||
pub enum InfraError {
|
||||
#[error("Database error")]
|
||||
Database(#[from] sqlx::Error),
|
||||
|
||||
#[error("HTTP request failed")]
|
||||
Http(#[from] reqwest::Error),
|
||||
}
|
||||
|
||||
// Application layer combines both
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
#[error("Domain error: {0}")]
|
||||
Domain(#[from] DomainError),
|
||||
|
||||
#[error("Infrastructure error: {0}")]
|
||||
Infra(#[from] InfraError),
|
||||
|
||||
#[error("Application error: {0}")]
|
||||
Application(String),
|
||||
}
|
||||
```
|
||||
|
||||
**Suggestion Template**:
|
||||
```
|
||||
For layered architectures, create error types for each layer:
|
||||
|
||||
// Domain layer
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DomainError {
|
||||
#[error("Invalid data: {0}")]
|
||||
Invalid(String),
|
||||
}
|
||||
|
||||
// Infrastructure layer
|
||||
#[derive(Error, Debug)]
|
||||
pub enum InfraError {
|
||||
#[error("Database error")]
|
||||
Database(#[from] sqlx::Error),
|
||||
}
|
||||
|
||||
// Application layer combines both
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
#[error("Domain: {0}")]
|
||||
Domain(#[from] DomainError),
|
||||
|
||||
#[error("Infra: {0}")]
|
||||
Infra(#[from] InfraError),
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Pattern 7: Generic Error Types
|
||||
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum OperationError<T>
|
||||
where
|
||||
T: std::error::Error + 'static,
|
||||
{
|
||||
#[error("Operation failed")]
|
||||
Failed(#[source] T),
|
||||
|
||||
#[error("Timeout after {0} seconds")]
|
||||
Timeout(u64),
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 8: Conditional Compilation
|
||||
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MyError {
|
||||
#[error("IO error")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
#[error("Database error")]
|
||||
Database(#[from] sqlx::Error),
|
||||
|
||||
#[cfg(feature = "redis")]
|
||||
#[error("Cache error")]
|
||||
Cache(#[from] redis::RedisError),
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 9: Enum with Unit and Complex Variants
|
||||
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ValidationError {
|
||||
// Unit variant
|
||||
#[error("Value is required")]
|
||||
Required,
|
||||
|
||||
// Tuple variant
|
||||
#[error("Invalid format: {0}")]
|
||||
InvalidFormat(String),
|
||||
|
||||
// Struct variant
|
||||
#[error("Out of range (expected {expected}, got {actual})")]
|
||||
OutOfRange { expected: String, actual: String },
|
||||
|
||||
// Nested error
|
||||
#[error("Validation failed")]
|
||||
Nested(#[from] SubValidationError),
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### DO: Clear, Actionable Error Messages
|
||||
|
||||
```rust
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ConfigError {
|
||||
// ✅ Clear and actionable
|
||||
#[error("Config file not found at '{path}'. Create one using: config init")]
|
||||
NotFound { path: String },
|
||||
|
||||
// ✅ Explains what's wrong and expected format
|
||||
#[error("Invalid port number '{port}'. Expected a number between 1 and 65535")]
|
||||
InvalidPort { port: String },
|
||||
}
|
||||
```
|
||||
|
||||
### DON'T: Vague Error Messages
|
||||
|
||||
```rust
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BadError {
|
||||
// ❌ Too vague
|
||||
#[error("Error")]
|
||||
Error,
|
||||
|
||||
// ❌ Not helpful
|
||||
#[error("Something went wrong")]
|
||||
Failed,
|
||||
}
|
||||
```
|
||||
|
||||
### DO: Include Context
|
||||
|
||||
```rust
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
// ✅ Includes what, where, and source
|
||||
#[error("Failed to read file '{path}'")]
|
||||
ReadFailed {
|
||||
path: String,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### DO: Type Aliases for Result
|
||||
|
||||
```rust
|
||||
pub type Result<T> = std::result::Result<T, MyError>;
|
||||
|
||||
// Now you can use:
|
||||
pub fn operation() -> Result<Value> {
|
||||
Ok(value)
|
||||
}
|
||||
```
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### Mistake 1: Forgetting #[source]
|
||||
|
||||
```rust
|
||||
// ❌ BAD: Source error not marked
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MyError {
|
||||
#[error("Failed")]
|
||||
Failed(std::io::Error), // Missing #[source]
|
||||
}
|
||||
|
||||
// ✅ GOOD: Properly marked
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MyError {
|
||||
#[error("Failed")]
|
||||
Failed(#[source] std::io::Error),
|
||||
}
|
||||
```
|
||||
|
||||
### Mistake 2: Using #[from] When You Need Custom Construction
|
||||
|
||||
```rust
|
||||
// ❌ Can't add context with #[from]
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MyError {
|
||||
#[error("Failed")]
|
||||
Failed(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
// ✅ Use #[source] for custom construction
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MyError {
|
||||
#[error("Failed to read config file '{path}'")]
|
||||
ConfigReadFailed {
|
||||
path: String,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Your Approach
|
||||
|
||||
1. **Detect**: Identify error type definitions or thiserror usage
|
||||
2. **Analyze**: Check message clarity, source chaining, and conversions
|
||||
3. **Suggest**: Provide specific improvements
|
||||
4. **Educate**: Explain when to use #[from] vs #[source] vs #[transparent]
|
||||
|
||||
## Communication Style
|
||||
|
||||
- Suggest thiserror for any custom error type
|
||||
- Explain the difference between #[from], #[source], and #[transparent]
|
||||
- Provide complete error type examples
|
||||
- Show how the error will be displayed
|
||||
- Point out missing error chains
|
||||
|
||||
When you see custom error types, immediately suggest thiserror patterns that will make them more ergonomic and idiomatic.
|
||||
Reference in New Issue
Block a user