rust-errors について
Rust のコーディングエージェント向けに、エラー型、伝播、panic の方針を扱うスキルです。公開 API 境界の内外に応じた設計指針と検証手順を示します。
主な機能
- 01panic と回復可能なエラーの区別
- 02列挙型と公開エラー構造体の設計
- 03`?` によるエラー伝播
- 04呼び出し側の対応に基づく分類
- 05Clippy とテストによる確認
活用シーン
- 01Rust ライブラリのエラー処理レビュー
- 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 Errors
エラー型は、何が起こり得るのか、呼び出し側がそれにどう対処するのかを定める契約です。どの失敗がパニックになり、どれがResultになり、エラーがどのような形を取り、呼び出しスタックを越えてどれだけのコンテキストが残るのかを定めます。失敗を起こり得ないようにする方法は/type-driven-design、型の形状の変更が呼び出し側を壊すかどうかは/rust-api-designを参照してください。
パニックの方針
unwrapとexpectは、エラー処理ではなく不変条件についてのアサーションです。許容されるのは、テスト内、起動前提条件に関するmain内、そしてコンパイラーには確認できないチェックの後です。後者では、expectで不変条件を明示し、値も含めます。許容されないのは、ライブラリコードで入力に由来する値に使う場合や、「失敗しないはず」というだけで理由を示さない場合です。
すべての失敗がResultになるわけではありません。呼び出し側が回復できる経路のない内部不変条件の破綻では、パニックを起こすべきです。プログラムは、型システムが到達しないと保証していた状態に、すでに陥っています。入力検証はその反対です。入力がどのようなものであっても呼び出し側が対応できるため、常にResultにします。
境界までResultを伝播させる
エラーは、再試行、報告、終了などを実際に判断できるレイヤーまで?で伝播させます。?はFromを介して変換するため、列挙型のバリアントに#[from]を付ければ、各呼び出し箇所のmap_errクロージャーが不要になります。
どの形にするか:境界で決める
判断基準はひとつです。このエラーは、リリースをまたいで保守する公開APIの境界を越えますか?
いいえ — 内部クレート、失敗の種類が限定されている場合、呼び出し側が出力するだけのエラーなら、列挙型が適しています。より簡潔で、より良い選択です。
#[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にあります。
公開列挙型は、この境界で4つの独立した問題を引き起こします。Parse(toml::de::Error)のようなバリアントは、使用するTOMLクレートを永続的に約束することになります。失敗の種類を追加すると、呼び出し側のマッチが壊れます。#[non_exhaustive]を付けても解決しません。最初からすべての呼び出し側に_ =>アームを要求するため、列挙型を公開する理由だった網羅的なマッチができなくなるからです。また、ConfigError::Io(e) if e.kind() == NotFoundから「設定ファイルが見つからない」という意味を呼び出し側に導き直させ、作業を押しつけます。前例はstd::io::Errorで、まさにこのパターンです。
いずれの場合も、thiserrorは#[source]による連鎖やDisplayの転送といった定型処理を担います。バイナリではanyhowを使います。バイナリのエラーは人が一度読むメッセージなので、コンテキストを付けたボックス化された動的エラーで十分です。内部ライブラリクレートを持つほど大きなバイナリでは、内部ではthiserrorを使い、最上位でのみanyhowを使います。公開ライブラリでanyhowを使うと、下流の呼び出し側は具体的な型に対するマッチを諦めざるを得ません。使い分ける本当の理由は好みではなく、これです。
エラーの分類
列挙型は、エラーがどこで発生したかではなく、呼び出し側がどう対処するかで分けます。常に同じ処理をする2つのバリアントは、ひとつのバリアントにまとめます。再試行可能なバリアントと致命的なバリアントがある場合、メッセージ文字列とのマッチングをせずに区別できる必要があります。大まかな列挙型にis_retryable()やkind()アクセサーを用意します。公開構造体のエラーでは、種類が非公開で、そこに追加しても破壊的変更にならないため、拡張性の問題は生じません。上記の指針に反して列挙型を公開する場合の#[non_exhaustive]は、網羅的なマッチングを犠牲にして、破壊的変更なしの拡張を可能にします。semverのルールは/rust-api-designを参照してください。
パニックが正しい選択となる場合
パニックはプログラムを停止させるという意味であり、「エラーを大声で返す」ことではありません。検出されたバグ、つまり内部不変条件への違反は、Error を返すのではなくパニックを発生させます。それに対して正しく対処できる呼び出し元が存在しないためです。catch_unwind は最後の手段です。パニックが発生したユニットを制御された方法で再起動し、何事もなかったかのように処理を続けることはありません。リリースビルドでチェックするコストが高すぎる不変条件については、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 属性があればそのまま維持し、それより厳しいレベルを上乗せすることはありません。次に、エージェント自身が対象を絞って監査し、以下を読みます。
rg '\.unwrap\(\)|\.expect\(' --glob '!**/tests/**' --glob '!**/benches/**' src/
残った各検出項目には、1 行の説明が必要です。clippy::unwrap_used と clippy::expect_used は restriction lint です。リポジトリの lint 設定への追加を提案するにとどめ、独断で有効にしたり、すでに設定されているレベルを上書きしたりしないでください。