anyhow 与 thiserror
你会学到什么
thiserror用派生宏自动实现Display和Error,省去样板代码。anyhow提供一个“万能错误类型”,适合应用层快速聚合。- 经验法则:库用 thiserror(精确),应用用 anyhow(方便)。
最小示例
use thiserror::Error;
#[derive(Error, Debug)]
enum ConfigError {
#[error("配置为空")]
Empty,
#[error("端口不是数字: {0}")]
NotANumber(String),
}
运行代码
cd examples
cargo run -p rt_35_error_libs
cargo test -p rt_35_error_libs
依赖:
[dependencies]
anyhow = "1"
thiserror = "2"
代码讲解
回忆“自定义错误类型”那一章,我们手写了 Display 和 Error。thiserror 用 #[error("...")] 把这些一行搞定,{0} 引用元组字段:
#[error("端口超出范围: {0}")]
OutOfRange(u32),
应用层函数返回 anyhow::Result<T>,? 能自动把任何实现了 Error 的类型转换进去:
fn load_config(input: &str) -> anyhow::Result<u16> {
let port = parse_port(input)?; // ConfigError 自动转成 anyhow::Error
Ok(port)
}
anyhow 还支持 .context("加载配置失败") 给错误加上下文,排查问题时非常有用。
常见错误
在库的公开 API 里直接用 anyhow::Error 当返回类型,会让调用方无法按变体匹配错误。库应当暴露具体的 thiserror 类型。
练习
- 给
ConfigError加一个#[error("缺少字段: {0}")]变体。 - 在
load_config里用.context(...)给错误补充上下文。
小结
thiserror 让定义精确错误几乎零样板,anyhow 让应用层聚合错误轻松自如,二者常配合使用。
下一步
接着学习用 clap 解析命令行参数。
完整示例代码
下面是 examples/35_error_libs/ 的完整源码。无需 clone 仓库,直接在页面上阅读、复制、对照运行。
examples/35_error_libs/src/main.rs
//! thiserror 定义错误类型,anyhow 在应用层聚合错误。
use thiserror::Error;
/// thiserror 自动生成 Display 和 Error 实现。
#[derive(Error, Debug, PartialEq)]
enum ConfigError {
#[error("配置为空")]
Empty,
#[error("端口不是数字: {0}")]
NotANumber(String),
#[error("端口超出范围: {0}")]
OutOfRange(u32),
}
/// 库层:返回精确的自定义错误。
fn parse_port(input: &str) -> Result<u16, ConfigError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(ConfigError::Empty);
}
let value: u32 = trimmed
.parse()
.map_err(|_| ConfigError::NotANumber(trimmed.to_string()))?;
if value > u16::MAX as u32 {
return Err(ConfigError::OutOfRange(value));
}
Ok(value as u16)
}
/// 应用层:用 anyhow::Result 聚合不同来源的错误,`?` 自动转换。
fn load_config(input: &str) -> anyhow::Result<u16> {
let port = parse_port(input)?;
Ok(port)
}
fn main() -> anyhow::Result<()> {
let port = load_config("8080")?;
println!("port = {port}");
println!("error demo: {}", parse_port("abc").unwrap_err());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_valid_port() {
assert_eq!(parse_port("8080"), Ok(8080));
}
#[test]
fn reports_specific_errors() {
assert_eq!(parse_port(""), Err(ConfigError::Empty));
assert_eq!(
parse_port("abc"),
Err(ConfigError::NotANumber("abc".to_string()))
);
assert_eq!(parse_port("70000"), Err(ConfigError::OutOfRange(70000)));
}
#[test]
fn anyhow_wraps_error() {
assert!(load_config("abc").is_err());
assert_eq!(load_config("443").unwrap(), 443);
}
} examples/35_error_libs/Cargo.toml
[package]
name = "rt_35_error_libs"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
anyhow = "1"
thiserror = "2"