スキル一覧に戻る
imehr

rust-error-handling

by imehr

0🍴 0📅 2026年1月15日
GitHubで見るManusで実行

SKILL.md


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

// ✅ 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 ?

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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

// ❌ 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

// ❌ 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

// ❌ 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

// ❌ 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

// 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

// 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]

スコア

総合スコア

50/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です