跳转到正文
zeno's blog
返回

Rust 基础:anyhow 为什么值得单独使用

专题: Rust 基础

Table of contents

Open Table of contents

先看 Rust 错误处理的痛点

Rust 的 Result<T, E> 要求你指定具体的错误类型 E。当一个函数可能产生多种错误时,问题来了:

async fn login(pool: &PgPool, redis: &redis::Client) -> Result<String, ???> {
    let player = sqlx::query("...").fetch_one(pool).await?;   // sqlx::Error
    let token = jwt::encode(&claims, &key)?;                   // jwt::errors::Error
    let mut conn = redis.get_async_connection().await?;        // redis::RedisError
    conn.set("session", &token).await?;                        // redis::RedisError
    Ok(token)
}
// ??? 填什么?三种不同的错误类型

没有 anyhow 的三种解决方案

方案 1:自定义枚举(正统做法)

#[derive(Debug)]
enum LoginError {
    Database(sqlx::Error),
    Auth(jwt::errors::Error),
    Cache(redis::RedisError),
}

impl From<sqlx::Error> for LoginError {
    fn from(e: sqlx::Error) -> Self { LoginError::Database(e) }
}
impl From<jwt::errors::Error> for LoginError {
    fn from(e: jwt::errors::Error) -> Self { LoginError::Auth(e) }
}
impl From<redis::RedisError> for LoginError {
    fn from(e: redis::RedisError) -> Self { LoginError::Cache(e) }
}
impl std::fmt::Display for LoginError { ... }
impl std::error::Error for LoginError { ... }

每多一种错误就加一个变体 + 一个 From 实现。10 个函数用到不同的错误组合 → 写到手软。

方案 2:Box<dyn std::error::Error>

async fn login(...) -> Result<String, Box<dyn std::error::Error>> {
    // 所有 ? 都能用了,任何实现了 Error 的类型都能装进 Box
    let player = sqlx::query("...").fetch_one(pool).await?;
    let token = jwt::encode(&claims, &key)?;
    Ok(token)
}

能用,但有几个问题:

方案 3:thiserror(方案 1 的简化版)

#[derive(thiserror::Error, Debug)]
enum LoginError {
    #[error("database error: {0}")]
    Database(#[from] sqlx::Error),
    #[error("auth error: {0}")]
    Auth(#[from] jwt::errors::Error),
    #[error("cache error: {0}")]
    Cache(#[from] redis::RedisError),
}

自动生成 FromDisplay。比方案 1 好多了,但每个函数可能需要不同的枚举。

anyhow 的方案

use anyhow::Result;

async fn login(pool: &PgPool, redis: &redis::Client) -> Result<String> {
    let player = sqlx::query("...").fetch_one(pool).await?;   // 直接用
    let token = jwt::encode(&claims, &key)?;                   // 直接用
    let mut conn = redis.get_async_connection().await?;        // 直接用
    conn.set("session", &token).await?;                        // 直接用
    Ok(token)
}

anyhow::Result<T> 就是 Result<T, anyhow::Error>anyhow::Error 能装任何实现了 std::error::Error 的类型,所有 ? 直接能用,不需要定义枚举,不需要写 From

anyhow 提供了什么

1. 任何错误类型都能装

use anyhow::Result;

fn do_stuff() -> Result<()> {
    std::fs::read_to_string("config.toml")?;  // io::Error     → anyhow::Error
    let n: i32 = "abc".parse()?;               // ParseIntError → anyhow::Error
    let conn = db.connect()?;                  // sqlx::Error   → anyhow::Error
    Ok(())
}
// 不需要任何 From 实现,anyhow 内部用 trait object 自动接住

2. 添加上下文(context)

裸错误信息经常不够用:

Error: No such file or directory (os error 2)

哪个文件?在做什么的时候出错的?

use anyhow::Context;

let config = std::fs::read_to_string("config.toml")
    .context("failed to read config file")?;
// Error: failed to read config file
// Caused by: No such file or directory (os error 2)

let port: u16 = config_str.parse()
    .with_context(|| format!("invalid port number: '{}'", config_str))?;
// Error: invalid port number: 'abc'
// Caused by: invalid digit found in string

context 包了一层说明,原始错误保留在 Caused by 链中。这在日志里排查问题时非常有用。

3. 手动构造错误

use anyhow::{anyhow, bail, ensure};

// anyhow! → 创建一个 anyhow::Error
return Err(anyhow!("player {} not found", player_id));

// bail! → 等于 return Err(anyhow!(...))
bail!("player {} not found", player_id);

// ensure! → 条件不满足时 bail
ensure!(player.health > 0, "player {} is dead", player.id);
// 等价于:
// if !(player.health > 0) {
//     bail!("player {} is dead", player.id);
// }

4. 错误链(cause chain)

let result = do_database_stuff()
    .context("failed to load player data")
    .context("login failed");

// 打印时:
// Error: login failed
// Caused by:
//   0: failed to load player data
//   1: connection refused (os error 111)

// 遍历错误链:
for cause in result.unwrap_err().chain() {
    println!("  caused by: {}", cause);
}

5. Backtrace

设置环境变量 RUST_BACKTRACE=1,anyhow 自动捕获 backtrace:

RUST_BACKTRACE=1 cargo run

# Error: login failed
# Caused by: connection refused
#
# Stack backtrace:
#    0: mini_tarkov_server::db::connect
#              at src/db.rs:42
#    1: mini_tarkov_server::handlers::login
#              at src/handlers.rs:15
#    ...

anyhow vs thiserror

这两个不是竞争关系,是配合使用的:

anyhow    → 应用层代码用(handler、main、业务逻辑)
             "我不关心具体错误类型,只关心错误信息和上下文"

thiserror → 库代码 / 需要匹配错误类型时用
             "调用者需要根据不同错误类型做不同处理"
// 库代码:用 thiserror 定义精确的错误类型
// 调用者可以 match 不同变体做不同处理
#[derive(thiserror::Error, Debug)]
pub enum AuthError {
    #[error("invalid credentials")]
    InvalidCredentials,
    #[error("token expired")]
    TokenExpired,
    #[error("account locked")]
    AccountLocked,
}

// 应用代码:用 anyhow 统一处理
// 不需要区分具体错误,加上下文后往上抛
async fn handler() -> anyhow::Result<Json<Player>> {
    let player = auth::verify(&token)
        .context("authentication failed")?;  // AuthError → anyhow::Error
    let items = db::get_items(player.id)
        .context("failed to load inventory")?;  // sqlx::Error → anyhow::Error
    Ok(Json(player))
}

在 axum handler 中使用 anyhow

anyhow::Error 没有实现 IntoResponse(anyhow 不知道 axum 的存在),需要包一层:

// 定义一次,全项目复用
struct AppError(anyhow::Error);

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        // 生产环境不要把内部错误信息暴露给客户端
        tracing::error!("{:#}", self.0);  // 日志里记完整错误链
        (StatusCode::INTERNAL_SERVER_ERROR,
         Json(serde_json::json!({"error": "internal server error"}))
        ).into_response()
    }
}

impl<E: Into<anyhow::Error>> From<E> for AppError {
    fn from(err: E) -> Self {
        AppError(err.into())
    }
}

// 现在所有 handler 都能用 ? 了
async fn handler(State(db): State<PgPool>) -> Result<Json<Player>, AppError> {
    let player = sqlx::query_as("SELECT ...")
        .fetch_one(&db)
        .await
        .context("failed to query player")?;  // ? 直接能用,context 也能用
    Ok(Json(player))
}

总结

没有 anyhow:
  每个函数要定义错误枚举 + From 实现 + Display 实现
  或者到处写 Box<dyn Error>
  没有上下文信息,出错了不知道是在干什么时候出的

有了 anyhow:
  返回值写 Result<T>(= Result<T, anyhow::Error>)
  所有 ? 直接能用
  .context("说明") 添加上下文
  自动 backtrace
  错误链完整保留

定位: 应用层的"万能错误容器"
  不是替代精确的错误类型(那是 thiserror 的事)
  而是让你不需要为每个函数的错误组合去定义类型

分享这篇文章:

上一篇
Rust 基础:异步编程内部机制
下一篇
Rust Web:服务器错误处理体系