关于 rust-errors
面向 Rust 编码 Agent 的错误处理技能,涵盖错误类型、传播方式和 panic 策略。它依据错误是否跨越公共 API 边界,指导库与二进制程序的设计,并提供验证步骤。
主要功能
- 01区分 panic 与可恢复错误
- 02指导枚举和公共错误结构体设计
- 03介绍用 `?` 传播错误
- 04按调用方处理方式划分错误
- 05提供 Clippy 和测试检查
适用场景
- 01审查 Rust 库的错误处理
- 02设计稳定的公共错误 API
- 03审计 `unwrap` 和 `expect` 用法
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rewrite-rs/ skills --skill rust-errors安装后在 Claude Code 中输入/rust-errors确认已加载。
附带文件agents/openai.yamlERROR-TYPES.md
详细介绍
Rust 错误
错误类型是一种契约,规定了可能出现什么问题,以及调用方如何处理:哪些失败会触发 panic,哪些会变成 Result,错误采用什么形式,以及调用栈中会保留多少上下文。如何让失败变得不可能,见 /type-driven-design;形式变更是否会破坏调用方,见 /rust-api-design。
panic 策略
unwrap 和 expect 是对不变量的断言,而不是错误处理。可以接受的场景:测试中、main 中的启动前提条件,以及检查过但编译器无法识别的情况——此时 expect 应说明不变量,并携带相关值。不可接受的场景:库代码中使用任何源自输入的值时,或任何仅以“这不可能失败”为理由的场景,却没有说明原因。
并非所有失败都应该用 Result 表示。对于无法由调用方恢复、且违反了内部不变量的情况,应触发 panic——程序已经处于类型系统保证不会到达的状态。输入校验则恰恰相反:无论输入如何,调用方都能做出响应,因此始终应返回 Result。
Result 一路传递到边界
错误通过 ? 传递到能够实际做出决策的层级——重试、报告或退出。? 会通过 From 进行转换,这就是为什么在枚举变体上使用 #[from],可以免去每个调用点的 map_err 闭包。
选择哪种形式:由边界决定
一个问题就能定夺:这个错误是否会跨越你要在多个版本中维护的公共 API 边界?
否——内部 crate、固定的一组失败情况,或调用方只会打印的错误——使用枚举。枚举是更简单、更好的选择:
#[derive(Debug, thiserror::Error)]
enum LoadError {
#[error("cannot read {path}")]
Read { path: PathBuf, #[source] source: std::io::Error },
#[error("malformed config in {path}")]
Parse { path: PathBuf, #[source] source: toml::de::Error },
}
是——已发布的库,其调用方升级时无需修改代码——使用包装私有类型的结构体,只公开调用方实际会询问的问题:
pub struct ConfigError { kind: ErrorKind }
impl ConfigError {
pub fn is_not_found(&self) -> bool { /* match on the private kind */ }
pub fn path(&self) -> &Path { /* ... */ }
}
完整且可编译的结构体模式见 ERROR-TYPES.md。
公共枚举在这个边界上有四个彼此独立的问题:像 Parse(toml::de::Error) 这样的变体,永久承诺了你使用的 TOML crate;新增失败模式会破坏调用方的匹配;#[non_exhaustive] 无法解决问题,因为它从一开始就迫使每个调用方添加 _ => 分支,从而失去了公开枚举的初衷——穷尽匹配;而且它把工作推给了调用方,让他们从 ConfigError::Io(e) if e.kind() == NotFound 这样的代码中重新推导出“配置文件缺失”。std::io::Error 就是这种模式的先例。
无论选择哪种形式,thiserror 仍会处理机械性的工作——#[source] 链接和 Display 转发。二进制程序使用 anyhow:二进制程序的错误消息只需让人读一次,因此带上下文的装箱动态错误就足够了。规模足以包含内部库 crate 的二进制程序会在这些库中使用 thiserror,只在顶层使用 anyhow;而已发布的库使用 anyhow,会迫使所有下游调用方放弃对具体类型的匹配——这才是两者分工的真正原因,而非个人偏好。
错误分类
应根据调用方如何处理错误来拆分枚举,而不是根据错误在哪儿产生。如果两个变体总是采用相同处理方式,就应合并为一个变体。如果一个变体可以重试,另一个则是致命错误,就必须能在不匹配错误消息字符串的情况下区分它们——可以在粗粒度枚举上提供 is_retryable() 或 kind() 访问器。对于公共结构体错误,不必担心新增类型的问题,因为类型是私有的,向其中添加内容不会构成破坏性变更;#[non_exhaustive] 适用于尽管有上述建议仍选择公开的枚举,它以放弃穷尽匹配为代价,换取非破坏性扩展。semver 规则见 /rust-api-design。
恰当的 panic
panic 意味着停止程序,而不是“响亮地返回错误”:检测到 bug——违反了内部不变量——就会触发 panic,而不是返回 Error,因为没有调用方能对此采取正确的处理。catch_unwind 是最后的手段,之后应受控地重启触发 panic 的单元,绝不能若无其事地继续运行。对于在发布构建中检查成本过高的不变量,debug_assert! 会在开发和测试构建中保留检查。
延后处理
让非法状态无法表示属于 /type-driven-design;形态变更是否构成 semver 破坏性变更属于 /rust-api-design;取消和超时属于 /async-rust;表达式级清理属于 /idiomatic-rust。
验证
cargo clippy --all-targets # add -- -D warnings only if the repo has no lint config
cargo test
按仓库中配置的 lint 级别运行 clippy——clippy.toml 或其自身的 lint 属性应保持不变,也不要再叠加更严格的级别。然后由 Agent 自行运行并检查以下针对性审计:
rg '\.unwrap\(\)|\.expect\(' --glob '!**/tests/**' --glob '!**/benches/**' src/
每个仍然存在的命中项都需要一行说明理由。clippy::unwrap_used 和 clippy::expect_used 属于 restriction lint:应提议将它们加入仓库的 lint 配置,不要擅自启用,也不要覆盖已经设置的级别。