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)
}
能用,但有几个问题:
- 类型签名长
- 没有 backtrace
- 添加上下文信息很别扭
方案 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),
}
自动生成 From 和 Display。比方案 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 的事)
而是让你不需要为每个函数的错误组合去定义类型