一个小小的 Rust 宏,彻底改变了我写错误处理的方式
前两天有个朋友问我,说他写 Rust 写得快疯了。我问咋了,他说你看看这代码:
fn process_user_input(input: &str) -> Result<User, String> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err("Empty input".to_string());
}
let user: User = serde_json::from_str(trimmed)
.map_err(|e| e.to_string())?;
if user.name.len() > 100 {
return Err("Name too long".to_string());
}
Ok(user)
}
我一看就乐了,这不就是我一年前的样子嘛。到处都是 return Err("xxx".to_string()),那个 .map_err(|e| e.to_string()) 写得人想吐。明明就是个简单的输入校验,愣是写成了填表格。
我跟他说,你试试 bail! 这个宏。他一脸懵,啥玩意儿?
我说你先别问,把代码改成这样:
use anyhow::{bail, Result};
fn process_user_input(input: &str) -> Result<User> {
let trimmed = input.trim();
if trimmed.is_empty() {
bail!("Empty input");
}
let user: User = serde_json::from_str(trimmed)?;
if user.name.len() > 100 {
bail!("Name too long");
}
Ok(user)
}

图1: 错误处理,从一团乱麻到清清爽爽
他看了半天,说就这?我说对啊就这。没有 .map_err(...) 了,没有 return Err(...) 了,想报错直接 bail!("错误信息") 完事。输入为空?bail!名字太长?bail!读起来跟说人话似的。
他问这宏里面是啥黑魔法,我说你看:
macro_rules! bail {
($e:expr) => {
return Err($crate::Error::msg($e));
};
}

图2: bail! 把繁琐的 return Err(…) 打包成一句话
就这么几行,它干的事儿就是帮你把 return Err(...) 包了一层。你可以理解成你家楼下开了个快递驿站,以前寄快递得自己找盒子、写单子、跑快递点,现在直接把东西往驿站一扔说声"寄北京"就完事了。本质没变,但体验完全不一样。
他说行,简单的我看懂了,复杂点的呢?我说那你看这个,解析命令行、读配置、启动服务,一套组合拳:
// 不用 bail! 的写法
fn run() -> Result<(), String> {
let args = parse_args().map_err(|e| e.to_string())?;
if !args.config_path.exists() {
return Err("Config file not found".to_string());
}
let config = load_config(&args.config_path)
.map_err(|e| format!("Failed to load config: {}", e))?;
if config.threads == 0 {
return Err("Invalid thread count".to_string());
}
start_process(config).map_err(|e| e.to_string())
}
再看用了 bail! 的:
use anyhow::{bail, Result};
fn run() -> Result<()> {
let args = parse_args()?;
if !args.config_path.exists() {
bail!("Config file not found");
}
let config = load_config(&args.config_path)?;
if config.threads == 0 {
bail!("Invalid thread count");
}
start_process(config)?;
Ok(())
}

图3: 流程没变,但分支更干净:出错就 bail!
他看完说,这确实清爽。解析参数、检查文件、加载配置、检查线程数、启动进程,每一步干啥一目了然,出错就 bail,不废话。
然后他问了个我当时也问过的问题:这玩意儿性能咋样?毕竟写 Rust 的谁不是性能控呢。
我说我测过,跑了一万次函数调用,手写 Result<T, String> 平均 123 纳秒,用 anyhow::Result 加 bail! 是 125 纳秒。内存的话一个 24 字节一个 32 字节。

图4: 性能差距几乎可以忽略:更在意可读性就大胆用
他说就差这点?我说对啊,2 纳秒的事儿。除非你在写操作系统内核或者搞高频交易,不然这点差距谁在乎。CLI 工具、Web 后端、内部服务,随便用。
他又问,那啥时候不该用呢?我说也有,比如你在写库代码需要精确控制错误类型的,或者 no_std 环境,那还是老老实实手写。但大部分业务代码,校验多、逻辑复杂的那种,用 bail! 准没错。

图5: 业务代码、CLI、Web 后端:bail! 很好用;库代码/ no_std:谨慎
我又给他看了个我常用的写法,参数校验:
fn validate(user: &User) -> Result<()> {
if user.name.trim().is_empty() {
bail!("Name cannot be empty");
}
if user.age < 18 {
bail!("User must be at least 18 years old");
}
Ok(())
}
他说这读起来跟念规则似的,名字不能空,年龄得满 18。我说对,这就是 bail! 的好处,代码即文档。
聊完他说回去就改,我说行,改完你就知道了,以后凌晨三点 debug 的时候会感谢自己的。
其实有时候改变你写代码方式的,不是什么高深的设计模式,就是这么一个小工具。现在我每次写函数都会想一下,这里能不能用 bail! 让代码更清爽点。能就用,简单。
对了,你们平时是怎么处理错误的?有没有其他用着顺手的小技巧?评论区聊聊呗。觉得有用的话顺手点个赞转发一下,让更多还在跟 map_err 搏斗的兄弟看到。