目录
- 1. 安装 anyhow
- 2. anyhow 解决什么问题?
- 3. 最基本用法:anyhow::Result
- 4. ? 自动转换错误
- 5. 最重要的功能:添加上下文 context
- 6. context 和 with_context 的区别
- 7. 创建临时错误:anyhow!
- 8. 提前返回错误:bail!
- 9. 条件校验:ensure!
- 10. 常用导入方式
- 11. 一个完整案例:读取配置文件并校验
- 12. anyhow 和普通 Result 的区别
- 13. 打印错误信息
- 14. 查看错误链 chain()
- 15. backtrace 回溯
- 16. 和 thiserror 搭配使用
- 17. 在 Web 项目中怎么用?
- 18. 在 SeaORM / MySQL 项目中的用法
- 19. 什么时候适合用 anyhow?
- 20. 常见错误写法
- 21. 推荐项目写法
- 22. 总结口诀
下面给你一份 Rust anyhow 详细使用教程。
anyhow 主要用于 应用程序层面的错误处理,它提供了 anyhow::Error 和 anyhow::Result<T>,可以让你在业务代码、命令行工具、Web 后端、脚本型程序里更轻松地处理各种错误。官方文档说明,anyhow::Error 是一个基于 trait object 的错误类型,适合 Rust 应用程序中进行惯用的错误处理;anyhow::Result<T> 等价于 Result<T, anyhow::Error>。(文档.rs)
1. 安装 anyhow
在 Cargo.toml 中添加:
[dependencies] anyhow = "1.0"
或者使用命令:
cargo add anyhow
当前 docs.rs 上 anyhow 最新文档版本显示为 1.0.103,但实际项目里一般写 1.0 即可,让 Cargo 自动选择兼容版本。(文档.rs)
2. anyhow 解决什么问题?
普通 Rust 代码里,经常会遇到不同来源的错误:
std::io::Error serde_json::Error parseIntError sqlx::Error sea_orm::DbErr
如果不用 anyhow,你可能需要自己定义一个大枚举:
enum AppError {
Io(std::io::Error),
Json(serde_json::Error),
Db(sea_orm::DbErr),
}
这在大型项目里是合理的,但在应用层、工具脚本、后台任务中会比较繁琐。
anyhow 的目标是:我不关心错误的精确类型,只想把错误一路往上传,并附加清晰的上下文信息。
3. 最基本用法:anyhow::Result
标准库的 Result<T, E> 不需要引入,但是 anyhow::Result<T> 是第三方库提供的类型别名,所以需要:
use anyhow::Result;
示例:
use anyhow::Result;
fn read_config() -> Result<String> {
let content = std::fs::read_to_string("config.toml")?;
Ok(content)
}
fn main() -> Result<()> {
let config = read_config()?;
println!("{}", config);
Ok(())
}
这里的:
Result<String>
其实等价于:
std::result::Result<String, anyhow::Error>
官方文档也建议在可能失败的函数中使用 Result<T, anyhow::Error>,或者等价的 anyhow::Result<T>,并且可以用 ? 传播实现了 std::error::Error 的错误。(文档.rs)
4. ? 自动转换错误
假设我们既读取文件,又解析 JSON:
[dependencies]
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
代码:
use anyhow::Result;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct AppConfig {
name: String,
port: u16,
}
fn load_config() -> Result<AppConfig> {
let content = std::fs::read_to_string("config.json")?;
let config: AppConfig = serde_json::from_str(&content)?;
Ok(config)
}
fn main() -> Result<()> {
let config = load_config()?;
println!("{:#?}", config);
Ok(())
}
这里有两种错误:
std::io::Error serde_json::Error
但是函数只返回:
anyhow::Result<AppConfig>
anyhow 会把这些错误统一包装成 anyhow::Error。
5. 最重要的功能:添加上下文 context
只用 ? 有一个问题:错误信息可能太底层。
比如文件不存在,直接报:
No such file or directory
你不知道是哪个业务步骤失败了。
所以 anyhow 非常推荐加上下文:
use anyhow::{Context, Result};
fn read_config() -> Result<String> {
let content = std::fs::read_to_string("config.toml")
.context("读取配置文件 config.toml 失败")?;
Ok(content)
}
fn main() -> Result<()> {
let content = read_config()?;
println!("{}", content);
Ok(())
}
注意这里需要引入:
use anyhow::Context;
官方文档说明,context 可以为底层错误添加更容易排查问题的业务上下文,比如让“文件不存在”变成“读取某个配置文件失败,因为文件不存在”。(文档.rs)
6. context 和 with_context 的区别
6.1 context
适合固定文本:
use anyhow::{Context, Result};
fn read_file() -> Result<String> {
std::fs::read_to_string("app.toml")
.context("读取 app.toml 配置文件失败")
}
6.2 with_context
适合需要动态拼接字符串的场景:
use anyhow::{Context, Result};
fn read_file(path: &str) -> Result<String> {
std::fs::read_to_string(path)
.with_context(|| format!("读取文件失败: {}", path))
}
with_context 的闭包只有在真正发生错误时才会执行;官方文档也明确说明,它是惰性添加上下文,只有错误发生时才求值。(文档.rs)
7. 创建临时错误:anyhow!
有时候不是来自系统错误,而是你自己判断业务不合法:
use anyhow::{anyhow, Result};
fn get_user(id: u64) -> Result<String> {
if id == 0 {
return Err(anyhow!("用户 ID 不能为 0"));
}
Ok(format!("user-{}", id))
}
fn main() -> Result<()> {
let user = get_user(0)?;
println!("{}", user);
Ok(())
}
anyhow! 可以快速构造一个 anyhow::Error。官方文档说明,临时错误消息可以用 anyhow! 构造,并且支持字符串插值。(文档.rs)
8. 提前返回错误:bail!
下面这段:
return Err(anyhow!("用户 ID 不能为 0"));
可以简化成:
bail!("用户 ID 不能为 0");
完整示例:
use anyhow::{bail, Result};
fn check_age(age: u8) -> Result<()> {
if age < 18 {
bail!("年龄必须大于等于 18,当前年龄: {}", age);
}
Ok(())
}
fn main() -> Result<()> {
check_age(16)?;
Ok(())
}
bail! 的作用就是 立即返回一个错误,官方文档也说明它等价于 return Err(anyhow!(...))。(文档.rs)
9. 条件校验:ensure!
ensure! 类似于 assert!,但是区别很重要:
assert!(条件);
条件不满足时会 panic。
而:
ensure!(条件, "错误信息");
条件不满足时会返回 Err,不会 panic。
示例:
use anyhow::{ensure, Result};
fn create_user(username: &str, age: u8) -> Result<()> {
ensure!(!username.trim().is_empty(), "用户名不能为空");
ensure!(age >= 18, "用户年龄不能小于 18,当前年龄: {}", age);
println!("创建用户成功: {}", username);
Ok(())
}
fn main() -> Result<()> {
create_user("", 16)?;
Ok(())
}
官方文档说明,ensure! 在条件不满足时提前返回错误,行为类似 assert!,但它返回错误而不是 panic。(文档.rs)
10. 常用导入方式
一般项目里常见这样写:
use anyhow::{anyhow, bail, ensure, Context, Result};
分别对应:
Result // anyhow::Result<T> Context // .context() 和 .with_context() anyhow! // 构造临时错误 bail! // 立即 return Err ensure! // 条件不满足时 return Err
11. 一个完整案例:读取配置文件并校验
目录:
demo-anyhow/
├── Cargo.toml
├── config.json
└── src/
└── main.rs
Cargo.toml:
[package]
name = "demo-anyhow"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
config.json:
{
"app_name": "demo-api",
"port": 8080,
"database_url": "mysql://root:123456@localhost:3306/demo"
}
src/main.rs:
use anyhow::{ensure, Context, Result};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct AppConfig {
app_name: String,
port: u16,
database_url: String,
}
fn load_config(path: &str) -> Result<AppConfig> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("读取配置文件失败: {}", path))?;
let config: AppConfig = serde_json::from_str(&content)
.with_context(|| format!("解析 JSON 配置文件失败: {}", path))?;
validate_config(&config)?;
Ok(config)
}
fn validate_config(config: &AppConfig) -> Result<()> {
ensure!(
!config.app_name.trim().is_empty(),
"app_name 不能为空"
);
ensure!(
config.port > 0,
"port 必须大于 0,当前值: {}",
config.port
);
ensure!(
config.database_url.starts_with("mysql://"),
"database_url 必须是 MySQL 连接地址,当前值: {}",
config.database_url
);
Ok(())
}
fn main() -> Result<()> {
let config = load_config("config.json")?;
println!("配置加载成功: {:#?}", config);
Ok(())
}
这个例子里:
std::fs::read_to_string(path)?
可能产生文件读取错误。
serde_json::from_str(&content)?
可能产生 JSON 解析错误。
ensure!(...)
可能产生业务校验错误。
但它们都被统一成了:
anyhow::Result<T>
12. anyhow 和普通 Result 的区别
普通标准库写法:
fn read_file() -> std::result::Result<String, std::io::Error> {
let content = std::fs::read_to_string("a.txt")?;
Ok(content)
}
anyhow 写法:
use anyhow::Result;
fn read_file() -> Result<String> {
let content = std::fs::read_to_string("a.txt")?;
Ok(content)
}
区别是:
std::result::Result<T, E>
需要你明确写错误类型 E。
而:
anyhow::Result<T>
默认错误类型就是:
anyhow::Error
所以它只有一个泛型参数 T。
13. 打印错误信息
示例:
use anyhow::{Context, Result};
fn run() -> Result<()> {
std::fs::read_to_string("missing.txt")
.context("读取 missing.txt 失败")?;
Ok(())
}
fn main() {
if let Err(err) = run() {
eprintln!("普通显示: {}", err);
eprintln!("带原因链显示: {:#}", err);
eprintln!("Debug 显示: {:?}", err);
}
}
几种打印方式:
{}
只打印最外层错误。
{:#}
打印错误和原因链。
{:?}
Debug 格式,可能包含 backtrace。
官方文档说明,{} 只打印最外层错误或上下文;{:#} 会包含原因链;{:?} 在捕获了 backtrace 时会包含 backtrace 信息。(文档.rs)
14. 查看错误链 chain()
你也可以手动遍历错误链:
use anyhow::{Context, Result};
fn run() -> Result<()> {
std::fs::read_to_string("missing.txt")
.context("读取配置文件失败")?;
Ok(())
}
fn main() {
if let Err(err) = run() {
eprintln!("错误: {}", err);
for cause in err.chain().skip(1) {
eprintln!("原因: {}", cause);
}
}
}
适合你想自定义日志格式的时候。
15. backtrace 回溯
如果想看到错误发生的调用栈,可以设置环境变量。
Linux / macOS:
RUST_BACKTRACE=1 cargo run
或者只给错误开启 backtrace:
RUST_LIB_BACKTRACE=1 cargo run
Windows PowerShell:
$env:RUST_BACKTRACE=1 cargo run
官方文档说明,Rust 1.65 及以上,如果底层错误类型没有自己的 backtrace,anyhow 可以捕获并打印 backtrace;可以通过 RUST_BACKTRACE 和 RUST_LIB_BACKTRACE 控制显示。(文档.rs)
16. 和 thiserror 搭配使用
通常建议:
anyhow // 应用层错误处理 thiserror // 定义明确的业务错误类型
例如:
[dependencies] anyhow = "1.0" thiserror = "2"
代码:
use anyhow::{Context, Result};
use thiserror::Error;
#[derive(Debug, Error)]
enum UserError {
#[error("用户不存在,id: {0}")]
NotFound(u64),
#[error("用户状态异常: {0}")]
InvalidStatus(String),
}
fn find_user(id: u64) -> std::result::Result<String, UserError> {
if id == 0 {
return Err(UserError::NotFound(id));
}
Ok(format!("user-{}", id))
}
fn run() -> Result<()> {
let user = find_user(0)
.context("查询用户失败")?;
println!("用户: {}", user);
Ok(())
}
fn main() -> Result<()> {
run()
}
find_user 返回明确的业务错误:
std::result::Result<String, UserError>
应用层 run 使用:
anyhow::Result<()>
这样底层错误类型明确,上层调用又很方便。
官方文档也说明,anyhow 本身不提供 derive(Error) 宏,如果需要自定义错误类型,可以手写 std::error::Error 实现,或者使用 thiserror 这类独立宏库。(文档.rs)
17. 在 Web 项目中怎么用?
比如你做 Rust Web 后端,推荐分层使用:
handler 层:转换成 HTTP 响应 service 层:可以用 anyhow::Result repository 层:返回数据库错误或 anyhow::Result
示例:
use anyhow::{Context, Result};
struct User {
id: u64,
name: String,
}
async fn query_user_from_db(id: u64) -> Result<User> {
// 这里假设是真实数据库查询
if id == 0 {
anyhow::bail!("用户 ID 不能为 0");
}
Ok(User {
id,
name: "Albert".to_string(),
})
}
async fn get_user_service(id: u64) -> Result<User> {
let user = query_user_from_db(id)
.await
.with_context(|| format!("查询用户失败,id={}", id))?;
Ok(user)
}
然后在 handler 层不要直接把 anyhow::Error 原样暴露给前端,而是转成统一响应:
async fn handler() {
match get_user_service(0).await {
Ok(user) => {
println!("返回用户: {}", user.name);
}
Err(err) => {
eprintln!("内部错误: {:?}", err);
// 返回给前端时建议统一成:
// {"code": 500, "message": "服务器内部错误"}
}
}
}
原因是:anyhow 的错误信息通常包含内部细节,比如文件路径、SQL、配置项、调用链等,不应该直接返回给用户。
18. 在 SeaORM / MySQL 项目中的用法
你之前经常问 Salvo + SeaORM + MySQL,这种项目里可以这样用:
use anyhow::{Context, Result};
use sea_orm::{Database, DatabaseConnection};
pub async fn connect_db(database_url: &str) -> Result<DatabaseConnection> {
let db = Database::connect(database_url)
.await
.with_context(|| format!("连接数据库失败: {}", mask_db_url(database_url)))?;
Ok(db)
}
fn mask_db_url(url: &str) -> String {
// 简单脱敏示例,真实项目可以做得更完善
url.replace("root:123456", "root:******")
}
注意这里不要把数据库密码原样写进错误上下文,否则日志里会泄露密码。
更好的写法:
use anyhow::{Context, Result};
use sea_orm::{Database, DatabaseConnection};
pub async fn connect_db(database_url: &str) -> Result<DatabaseConnection> {
let db = Database::connect(database_url)
.await
.context("连接 MySQL 数据库失败")?;
Ok(db)
}
19. 什么时候适合用 anyhow?
适合:
命令行工具 后台任务 应用程序 main 函数 Web 项目的 service 层 快速开发阶段 内部工具 测试代码 脚本型程序
不太适合:
公共库的公开 API 需要让调用者精确 match 错误类型的场景 需要严格区分错误码的业务核心层
例如公共库最好不要这样:
pub fn parse_config() -> anyhow::Result<Config> {
// ...
}
因为别人调用你的库时,很难知道具体会返回哪些错误。
公共库更推荐:
pub fn parse_config() -> Result<Config, ConfigError> {
// ...
}
其中 ConfigError 可以用 thiserror 定义。
20. 常见错误写法
错误 1:忘记引入 Context
use anyhow::Result;
fn read_file() -> Result<String> {
std::fs::read_to_string("a.txt")
.context("读取失败")
}
会报错,因为 .context() 来自 Context trait。
正确:
use anyhow::{Context, Result};
fn read_file() -> Result<String> {
std::fs::read_to_string("a.txt")
.context("读取失败")
}
错误 2:把 anyhow::Result 当成标准库 Result 用
错误:
use anyhow::Result;
fn demo() -> Result<String, std::io::Error> {
Ok("hello".to_string())
}
anyhow::Result<T> 只有一个泛型参数。
正确写法 1:
use anyhow::Result;
fn demo() -> Result<String> {
Ok("hello".to_string())
}
正确写法 2:
fn demo() -> std::result::Result<String, std::io::Error> {
Ok("hello".to_string())
}
错误 3:滥用 unwrap
不推荐:
let content = std::fs::read_to_string("config.toml").unwrap();
推荐:
use anyhow::{Context, Result};
fn read_config() -> Result<String> {
let content = std::fs::read_to_string("config.toml")
.context("读取 config.toml 失败")?;
Ok(content)
}
21. 推荐项目写法
可以在项目里定义一个统一 prelude:
// src/prelude.rs
pub use anyhow::{anyhow, bail, ensure, Context, Result};
然后业务代码里:
use crate::prelude::*;
fn demo() -> Result<()> {
ensure!(1 + 1 == 2, "数学出错了");
Ok(())
}
如果是多模块项目,这种写法很舒服。
22. 总结口诀
anyhow 可以这样记:
Result<T> // 统一错误返回类型 ? // 自动向上传播错误 context // 给错误加固定上下文 with_context // 给错误加动态上下文 anyhow! // 手动创建错误 bail! // 直接 return Err ensure! // 条件不满足就 return Err
实际开发中最常用的是:
use anyhow::{Context, Result};
其次是:
use anyhow::{anyhow, bail, ensure};
对于你的 Rust Web 后端项目,我建议:
应用层 / service 层:anyhow::Result
领域错误 / 公共库:thiserror 自定义错误
handler 层:把 anyhow::Error 转换成统一 HTTP 响应
日志层:打印 {:#} 或 {:?}
返回前端:不要直接返回 anyhow 的内部错误