AgentSkillsCN

rust-error-handling

使用thiserror/anyhow实现Result、Option和自定义错误模式

SKILL.md
--- frontmatter
name: rust-error-handling
description: Result, Option, and custom error patterns with thiserror/anyhow
version: 1.0.0
triggers:
  - rust error
  - Result
  - Option
  - thiserror
  - anyhow
  - error handling rust
  - unwrap
  - expect

Rust Error Handling Guidelines

Overview

This skill provides patterns for error handling in Rust using Result, Option, and the ecosystem's best practices with thiserror (library errors) and anyhow (application errors).

Quick Reference

PatternWhen to UseExample
ResultRecoverable errorsResult<T, E>
OptionOptional valuesOption<T>
thiserrorLibrary error types#[derive(Error)]
anyhowApplication errorsanyhow::Result<T>
? operatorError propagationfile.read()?

When to Use What

ScenarioUse
Writing a librarythiserror with custom error types
Writing an applicationanyhow for convenience
Internal module errorsCustom enum with thiserror
Value might not existOption<T>
Operation can failResult<T, E>

Core Patterns

Pattern 1: Custom Error Types with thiserror

rust
// ✅ CORRECT: Library error type with thiserror
use thiserror::Error;

#[derive(Debug, Error)]
pub enum AppError {
    #[error("User not found: {0}")]
    NotFound(String),

    #[error("Validation error: {0}")]
    Validation(String),

    #[error("Unauthorized: {0}")]
    Unauthorized(String),

    #[error("Conflict: {0}")]
    Conflict(String),

    #[error("Database error")]
    Database(#[from] sqlx::Error),

    #[error("IO error")]
    Io(#[from] std::io::Error),

    #[error("Internal error: {0}")]
    Internal(String),
}

// Implement conversion to HTTP status codes (for web apps)
impl AppError {
    pub fn status_code(&self) -> u16 {
        match self {
            AppError::NotFound(_) => 404,
            AppError::Validation(_) => 400,
            AppError::Unauthorized(_) => 401,
            AppError::Conflict(_) => 409,
            AppError::Database(_) | AppError::Io(_) | AppError::Internal(_) => 500,
        }
    }
}

// For Axum: implement IntoResponse
impl axum::response::IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        let status = axum::http::StatusCode::from_u16(self.status_code())
            .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR);

        let body = serde_json::json!({
            "error": self.to_string()
        });

        (status, axum::Json(body)).into_response()
    }
}

Pattern 2: Error Propagation with ?

rust
// ✅ CORRECT: Clean error propagation
use crate::error::AppError;

pub async fn get_user_profile(user_id: i64) -> Result<UserProfile, AppError> {
    // ? automatically converts sqlx::Error to AppError via #[from]
    let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", user_id)
        .fetch_optional(&pool)
        .await?
        .ok_or_else(|| AppError::NotFound(format!("User {}", user_id)))?;

    let settings = get_user_settings(user_id).await?;
    let posts = get_user_posts(user_id).await?;

    Ok(UserProfile { user, settings, posts })
}

// ❌ WRONG: Explicit matching when ? would work
pub async fn get_user_bad(user_id: i64) -> Result<User, AppError> {
    let result = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", user_id)
        .fetch_optional(&pool)
        .await;

    match result {
        Ok(Some(user)) => Ok(user),
        Ok(None) => Err(AppError::NotFound("User not found".into())),
        Err(e) => Err(AppError::Database(e)),  // Unnecessary - #[from] does this
    }
}

Pattern 3: Option Handling

rust
// ✅ CORRECT: Option combinators
fn get_config_value(key: &str) -> Option<String> {
    std::env::var(key).ok()
}

fn get_port() -> u16 {
    get_config_value("PORT")
        .and_then(|s| s.parse().ok())
        .unwrap_or(3000)
}

fn get_database_url() -> Result<String, AppError> {
    get_config_value("DATABASE_URL")
        .ok_or_else(|| AppError::Internal("DATABASE_URL not set".into()))
}

// Chaining Options
fn get_user_email(users: &HashMap<i64, User>, id: i64) -> Option<&str> {
    users.get(&id).map(|u| u.email.as_str())
}

// Option to Result conversion
fn require_user(users: &HashMap<i64, User>, id: i64) -> Result<&User, AppError> {
    users.get(&id)
        .ok_or_else(|| AppError::NotFound(format!("User {}", id)))
}

Pattern 4: Application Errors with anyhow

rust
// ✅ CORRECT: anyhow for application code
use anyhow::{Context, Result, bail, ensure};

async fn process_file(path: &str) -> Result<Data> {
    // Context adds information to errors
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("Failed to read file: {}", path))?;

    let data: Data = serde_json::from_str(&content)
        .with_context(|| format!("Failed to parse JSON in: {}", path))?;

    // bail! for early returns with error
    if data.items.is_empty() {
        bail!("File {} contains no items", path);
    }

    // ensure! for assertions that return errors
    ensure!(data.version >= 2, "File format version {} is not supported", data.version);

    Ok(data)
}

// In main.rs - display full error chain
fn main() -> Result<()> {
    if let Err(err) = run() {
        eprintln!("Error: {:#}", err);  // {:#} shows full chain
        std::process::exit(1);
    }
    Ok(())
}

Pattern 5: Error Context and Wrapping

rust
// ✅ CORRECT: Adding context to errors
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ServiceError {
    #[error("Failed to create user '{email}'")]
    CreateUser {
        email: String,
        #[source]
        source: DbError,
    },

    #[error("Failed to send notification to user {user_id}")]
    Notification {
        user_id: i64,
        #[source]
        source: NotificationError,
    },
}

// Using it
pub async fn create_user(email: &str) -> Result<User, ServiceError> {
    repo.create(email)
        .await
        .map_err(|source| ServiceError::CreateUser {
            email: email.to_string(),
            source,
        })
}

Pattern 6: Handling Multiple Error Types

rust
// ✅ CORRECT: Unified error type for multiple sources
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ApiError {
    #[error("Database error")]
    Db(#[from] sqlx::Error),

    #[error("Serialization error")]
    Json(#[from] serde_json::Error),

    #[error("HTTP client error")]
    Http(#[from] reqwest::Error),

    #[error("Validation failed: {0}")]
    Validation(String),
}

// All these work with ?
async fn handler() -> Result<Response, ApiError> {
    let user = db.query().await?;           // sqlx::Error -> ApiError
    let json = serde_json::to_string(&user)?; // serde_json::Error -> ApiError
    let resp = client.post(url).send().await?; // reqwest::Error -> ApiError
    Ok(Response::new(json))
}

Anti-Patterns

Don't: Unwrap in Production Code

rust
// ❌ BAD: Panics on None/Err
let user = get_user(id).unwrap();
let port: u16 = env::var("PORT").unwrap().parse().unwrap();

// ✅ GOOD: Handle the error
let user = get_user(id).ok_or(AppError::NotFound("User"))?;
let port: u16 = env::var("PORT")
    .unwrap_or_else(|_| "3000".to_string())
    .parse()
    .unwrap_or(3000);

// ✅ ACCEPTABLE: unwrap() with guaranteed safety
let regex = Regex::new(r"^\d+$").unwrap();  // Compile-time known pattern

Don't: Ignore Errors

rust
// ❌ BAD: Silently ignoring errors
let _ = file.write_all(data);
let _ = send_notification(user_id);

// ✅ GOOD: At minimum, log errors
if let Err(e) = file.write_all(data) {
    tracing::error!("Failed to write file: {}", e);
}

// Or propagate
file.write_all(data)?;

Don't: Use String as Error Type

rust
// ❌ BAD: String errors lose type information
fn process() -> Result<(), String> {
    Err("Something went wrong".to_string())
}

// ✅ GOOD: Use proper error types
fn process() -> Result<(), AppError> {
    Err(AppError::Internal("Something went wrong".into()))
}

Don't: Panic in Libraries

rust
// ❌ BAD: Library code that panics
pub fn parse_config(s: &str) -> Config {
    serde_json::from_str(s).expect("Invalid config")  // Don't panic!
}

// ✅ GOOD: Return Result
pub fn parse_config(s: &str) -> Result<Config, ConfigError> {
    serde_json::from_str(s).map_err(ConfigError::Parse)
}

Common Conversions

rust
// Option -> Result
let value = some_option.ok_or(AppError::NotFound("missing"))?;
let value = some_option.ok_or_else(|| AppError::NotFound("missing"))?;

// Result -> Option
let value = some_result.ok();  // Discards error

// Map error type
let value = result.map_err(|e| AppError::Internal(e.to_string()))?;

// Add context (with anyhow)
let value = result.context("operation failed")?;
let value = result.with_context(|| format!("failed for {}", id))?;

Logging Errors

rust
// Log at appropriate levels
match result {
    Ok(value) => value,
    Err(e) => {
        match &e {
            AppError::NotFound(_) => tracing::debug!("Not found: {}", e),
            AppError::Validation(_) => tracing::info!("Validation: {}", e),
            AppError::Unauthorized(_) => tracing::warn!("Auth failed: {}", e),
            _ => tracing::error!("Error: {}", e),
        }
        return Err(e);
    }
}

Resources

TopicLink
Result Patterns[mdc:resources/result-patterns.md]
Option Patterns[mdc:resources/option-patterns.md]
thiserror Guide[mdc:resources/thiserror.md]
anyhow Guide[mdc:resources/anyhow.md]