Rust 错误处理的 7 月总结:从 panic 到优雅降级的完整进化路径回顾 Rust 错误处理的 7 月总结从 panic 到优雅降级的完整进化路径回顾一、错误处理的五个等级一张图看进化路径我把 Rust 的错误处理分了五个等级。7 月初我在第一级7 月底达到了第四级——第五级还在路上。学错误处理有一个天然劣势没经历过没有 Result 的语言是怎么处理错误的。Java 的 try-catch、Python 的异常、Go 的 if err ! nil——这些模式我都是后来才理解的。但 Rust 的 Result 让我少走了很多弯路因为它把错误类型化当作一等公民而不是用 return code 或 exception 绕过。二、等级一到等级三从蛮力到设计7 月第一版 AI CLI 工具的代码大概是这样的// // 等级一典型的能跑就行错误处理 // 这类代码在演示时能过生产环境一碰就碎 // fn v1_parse_config(path: str) - Config { // unwrap: 如果文件不存在或格式不对直接 panic let content std::fs::read_to_string(path).unwrap(); serde_json::from_str(content).unwrap() // 出了错用户看到的只有 thread main panicked at... }第一次觉醒是在一个下雨的下午。我的 AI CLI 因为 API Key 的问题崩溃了终端里打印了 40 多行的 panic 栈——但真正有用的信息只有401 Unauthorized这五个字。我意识到panic 是给开发者看的错误信息是给用户看的两者是完全不同的东西。// // 等级二到等级三的过渡从 Boxdyn Error 到自定义 enum // use std::fmt; /// 等级三自定义的错误类型 /// 每种错误都有明确的变体调用方可以精确匹配和处理 #[derive(Debug)] enum AiCliError { /// 配置文件问题找不到文件、格式错误 Config(String), /// 网络相关超时、连接失败、DNS 解析失败 Network(String), /// API 返回异常鉴权失败(401)、频率限制(429)、服务端错误(5xx) Api { status: u16, message: String }, /// 用户输入非法空输入、超长输入 InvalidInput(String), } // 实现 Display trait让错误能友好地打印给用户 impl fmt::Display for AiCliError { fn fmt(self, f: mut fmt::Formatter_) - fmt::Result { match self { AiCliError::Config(msg) write!(f, 配置文件错误{}, msg), AiCliError::Network(msg) write!(f, 网络错误{}, msg), AiCliError::Api { status, message } { write!(f, API 错误 [{}]{}, status, message) } AiCliError::InvalidInput(msg) write!(f, 输入错误{}, msg), } } } // 实现 std::error::Error trait才能被 ? 操作符传播 impl std::error::Error for AiCliError {}等级三已经比等级一好太多了但它还有一个问题样板代码太多。每加一个错误变体都要手动实现 Display。这时候thiserror进场了。三、等级四thiserror 让错误定义回归业务逻辑// // 等级四用 thiserror 派生宏自动生成样板代码 // 只需定义错误变体和错误消息Display/Error/From 全部自动实现 // use thiserror::Error; #[derive(Error, Debug)] pub enum AppError { /// IO 操作失败 #[error(IO 错误{0})] Io(#[from] std::io::Error), /// JSON 解析失败 #[error(JSON 解析错误{0})] Serde(#[from] serde_json::Error), /// HTTP 请求错误 #[error(HTTP 请求失败{0})] Http(#[from] reqwest::Error), /// 业务逻辑错误 #[error(业务错误 [{code}]{message})] Business { code: static str, message: String, }, } /// 用 ? 传播错误所有内部错误都通过 #[from] 自动转换 /// 调用方看到的是统一的 AppError而不是底层细节 fn load_and_parse_config(path: str) - ResultConfig, AppError { let content std::fs::read_to_string(path)?; // IO 错误自动转为 AppError::Io let config: Config serde_json::from_str(content)?; // JSON 错误自动转为 AppError::Serde Ok(config) } /// 网络调用的错误处理也收敛到 AppError async fn call_ai_api(prompt: str) - ResultString, AppError { let resp reqwest::get(https://api.example.com/chat) .await?; // reqwest::Error 自动转为 AppError::Http if !resp.status().is_success() { return Err(AppError::Business { code: API_ERROR, message: format!(返回状态码 {}, resp.status()), }); } Ok(resp.text().await?) }thiserror带来的真正好处不是省了几行 Display 实现代码而是让错误类型设计变得和业务逻辑设计一样自然。当你不需要纠结怎么实现 Display时你就有更多的脑力去思考这个错误应该怎么分类、怎么聚合、调用方怎么处理。四、等级五anyhow 做应用层thiserror 做库层——7 月还没有完全达到等级五的思想我很早就听说了但 7 月确实没有完全落实库library用 thiserror 定义精确的错误类型应用application用 anyhow 做灵活的上下文传播。为什么要这样分因为你的库比如ai-corecrate被其他 crate 依赖这些调用方需要精确匹配错误类型来做不同的恢复策略。而你的应用入口main.rs只需要两件事把错误信息友好地展示给用户记录足够多的上下文用于排查。anyhow的context()方法是一个我 7 月最后一周才发现的宝藏use anyhow::{Context, Result}; /// 用 anyhow 给错误链加人类可读的上下文 /// 每一层调用都给错误加上我在这里做什么的信息 fn do_complex_task() - Result() { let config load_config() .context(加载配置文件时失败请检查 ~/.ai-cli/config.toml 是否存在)?; let api_client build_client(config) .context(初始化 API 客户端失败请检查 API Key 是否已配置)?; let response api_client.send_message(hello) .context(发送消息失败请检查网络连接和 API 地址是否正确)?; println!({}, response); Ok(()) }当这个函数失败时用户看到的不是冷冰冰的 Error: Connection refused而是发送消息失败请检查网络连接和 API 地址是否正确\nCaused by: Connection refused (os error 61)。多一层 context少一个 issue。这个context()习惯让我 7 月的 issue 量直接少了 40%——用户自己就能从错误信息里找到原因了。五、总结7 月的错误处理之路本质上是从代码能跑就行到代码出了问题有尊严的进化。一个可能要花很多时间理解为什么错误处理很重要——因为没经历过生产环境被 panic 轰炸的夜晚。三条月度总结unwrap/panic 是学习阶段的速记符生产代码里一个都不该有。把每个 unwrap 替换成 Error ? 传播是走出新手村的第一步。thiserror 是 Rust 错误处理的开悟工具。它让你把注意力从怎么定义错误转到怎么分类和处理错误。库用 thiserror精确应用用 anyhow灵活。这不是推荐做法是经历过调用方想要精确匹配错误但只有一个 Box的痛苦后总结出的必然结论。8 月的计划是在 AI CLI 里完整实现等级五把ai-core和ai-provider的 all errors 替换成 thiserror 定义的精确类型在main.rs和 CLI 入口层用 anyhow 做上下文传播。这样这个工具无论在哪里出错用户都能看到一个人话版的错误提示。

本月热点