AgentSkillsCN

rust-error-handling

在设计错误类型、封装第三方错误、识别重复出现且错误类型相同的 map_err 调用、借助 error_set 构建模块化的错误层级、在自定义错误与 eyre::Report 之间做出抉择,或是在 Rust 中编写返回 Result 的函数时,此技能大有裨益。

SKILL.md
--- frontmatter
name: rust-error-handling
description: Use when designing error types, wrapping third-party errors, seeing repeated map_err calls with same error types, building modular error hierarchies with error_set, choosing between custom errors vs eyre::Report, or writing Result-returning functions in Rust.

Rust Error Handling

Core principle: Start simple. Add structure when pain emerges.

See also: error_set patterns for detailed error_set! syntax and examples.

Don't Over-Engineer

rust
// ✅ Start here: just use the error directly
fn read_config(path: &Path) -> Result<Config, std::io::Error> {
    let content = std::fs::read_to_string(path)?;
    toml::from_str(&content).map_err(|e| std::io::Error::other(e))
}

// ✅ Still fine: anyhow/eyre for applications
fn load_app() -> eyre::Result<App> {
    let config = read_config(Path::new("config.toml"))?;
    let db = connect_db(&config.db_url)?;
    Ok(App { config, db })
}

Only create custom error types when you feel the friction.

When to Create Custom Error Type

Signal: Repeated map_err with same types across multiple functions.

rust
// ❌ Pain: map_err everywhere
fn fetch_user() -> Result<User, ApiError> {
    let data = db.query().map_err(|e| ApiError::Internal(e.to_string()))?;
    let parsed = serde_json::from_str(&data).map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(parsed)
}

// ✅ Relief: error_set when pattern repeats 3+ times
error_set! {
    RepoError := {
        Db(sqlx::Error),
        Parse(serde_json::Error),
    };
}

fn fetch_user() -> Result<User, RepoError> {
    let data = db.query()?;
    let parsed = serde_json::from_str(&data)?;
    Ok(parsed)
}

When unwrap/expect is Appropriate

Fail-fast at startup. Propagate at runtime.

PhaseApproach
main() initializationexpect("reason")
Config/pool setupexpect("reason")
Request handlingResult + ?
Library codeResult + ?
rust
fn main() {
    let config = Config::from_env().expect("Failed to load config");
    let pool = Pool::connect(&config.database_url)
        .await
        .expect("Database connection required");

    run_server(config, pool).await.unwrap();
}

Static vs Runtime Context

Inner data only for runtime information.

rust
error_set! {
    ConfigError := {
        // ✅ Static message - no inner data
        #[display("Config file not found")]
        NotFound,

        // ❌ Redundant: message is always the same
        #[display("Invalid config: {0}")]
        InvalidConfig(String),  // if always "missing required field"

        // ✅ Runtime info - varies per call
        #[display("Missing field: {0}")]
        MissingField(String),   // "host", "port", "timeout"

        // ✅ Wrapped error
        #[display("IO error: {0}")]
        Io(std::io::Error),
    };
}

Data Firewall (Public APIs Only)

Don't leak third-party errors from public crate APIs.

rust
// ❌ Leaks sqlx in public API
pub fn get_user(id: &str) -> Result<User, sqlx::Error>

// ✅ Wrapped for public API
pub fn get_user(id: &str) -> Result<User, DbError>

Internal modules can use third-party errors directly.

Unexpected Errors

Use eyre::Report for truly unexpected errors with stack traces.

rust
error_set! {
    DomainError := {
        Unexpected(eyre::Report),
    };
}

something().map_err(|e| DomainError::Unexpected(eyre::eyre!(e).wrap_err("context")))?;

Logging

Error TypeLog Level
Business (InvalidEmail)INFO/WARN
System (ConnectionFailed)ERROR
UnexpectedERROR + trace

Log once at the edge, not at each layer.