Initial commit
This commit is contained in:
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.
|
||||
Reference in New Issue
Block a user