mcpField
R
rust-errors

rust-errors

github.com/rewrite-rs其他1041 次安装10 周前更新
代码仓库

关于 rust-errors

面向 Rust 编码 Agent 的错误处理技能,涵盖错误类型、传播方式和 panic 策略。它依据错误是否跨越公共 API 边界,指导库与二进制程序的设计,并提供验证步骤。

主要功能

  1. 01区分 panic 与可恢复错误
  2. 02指导枚举和公共错误结构体设计
  3. 03介绍用 `?` 传播错误
  4. 04按调用方处理方式划分错误
  5. 05提供 Clippy 和测试检查

适用场景

  1. 01审查 Rust 库的错误处理
  2. 02设计稳定的公共错误 API
  3. 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 配置,不要擅自启用,也不要覆盖已经设置的级别。

评论

支持 Markdown