はじめに
Rust で「ライブラリのエラー型をどう設計するか」は永遠のテーマです。素朴な答えは2つあります。
- アプリケーションなら
anyhowで済ませる - ライブラリなら
thiserrorで型付きエラーを返す
これは2026年でも変わりません。この記事は「ライブラリ向け」のほうの設計を、 #[non_exhaustive] を組み合わせて SemVer 互換を壊さない形で書く方法を扱います。
まず動くコード
use std::num::ParseIntError; #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum LoadError { #[error("io: {0}")] Io(#[from] std::io::Error), #[error("parse: {0}")] Parse(#[from] ParseIntError), #[error("empty input")] Empty, } pub fn parse_first_number(text: &str) -> Result<i64, LoadError> { let line = text.lines().next().ok_or(LoadError::Empty)?; let n = line.trim().parse::<i64>()?; // ParseIntError → LoadError 自動変換 Ok(n) } fn main() -> Result<(), LoadError> { println!("{}", parse_first_number("42\nrest")?); println!("{:?}", parse_first_number("")); println!("{:?}", parse_first_number("not-a-number")); Ok(()) }
実行結果は次のとおりです。
42
Err(Empty)
Err(Parse(ParseIntError { kind: InvalidDigit }))
Cargo.toml に必要なものは1行だけです。
[dependencies] thiserror = "2"
thiserror 2.0 は 2024年末リリースで、Rust 2021/2024 両方で動きます。
thiserror の役割
thiserror::Error を #[derive] すると以下が自動で実装されます。
std::error::Error(=Rust の標準エラートレイト)std::fmt::Display(#[error("...")]属性で書式を指定)From<T>(#[from]属性を付けたフィールドから自動変換)
書く側は Debug を #[derive] して、各 variant に #[error("メッセージ")] を付けるだけ。
#[derive(Debug, thiserror::Error)] pub enum MyError { #[error("bad request: {reason}")] BadRequest { reason: String }, }
{reason} のような placeholder には struct のフィールド名や、 タプル variant のインデックス ({0}, {1}) を使えます。
#[from] でエラー変換を ? で連鎖させる
#[from] は ? 演算子で自動変換するための From 実装 を生成します。これにより、内部エラーを呼び出し側に透過的に伝搬できます。
#[derive(Debug, thiserror::Error)] pub enum LoadError { #[error("io: {0}")] Io(#[from] std::io::Error), } fn read() -> Result<String, LoadError> { let s = std::fs::read_to_string("config.toml")?; // io::Error → LoadError::Io Ok(s) }
? を書くだけで std::io::Error が LoadError::Io(...) に変換されます。手書きの map_err は要りません。
注意点があります。同じ型を #[from] 付きで複数の variant に置くことはできません。「io::Error を Io と Disk の両 variant に入れたい」ならどちらかは手書きの From 実装にします。
#[non_exhaustive] で SemVer を守る
ここが重要です。公開ライブラリのエラー型に #[non_exhaustive] を付けると、利用者の match 文に _ ワイルドカード arm を強制できます。
// あなたのライブラリ側 #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum LoadError { #[error("io: {0}")] Io(#[from] std::io::Error), #[error("empty input")] Empty, }
利用者側の match はこうなります。
// 利用者側 match error { LoadError::Io(_) => println!("io error"), LoadError::Empty => println!("empty"), _ => println!("other"), // ← この `_` がないとコンパイルエラー }
_ を強制することで、あなたのライブラリが 将来 variant を追加しても利用者のコードは壊れません。これが #[non_exhaustive] の最大のメリットです。新しい variant の追加は SemVer 上「破壊的変更」になりえますが、#[non_exhaustive] 付きなら minor バージョン (1.0 → 1.1) で追加できます。
注意点を2つ挙げます。
- ライブラリ内部 (同じ crate 内) では
_を強制されない。crate 外からの match だけ制約される - struct でも使える (新フィールド追加に強くなる)
ライブラリ vs アプリの使い分け
ここで最初の二択に戻ります。
| 用途 | 選択 | 理由 |
|---|---|---|
| 公開ライブラリ | thiserror で型付きエラー |
利用者がエラー種別ごとにハンドリングできる |
| アプリ (CLI, サーバー) | anyhow で型消去 |
エラーを上位に投げて main で ? するだけで済む |
| 内部 lib (同じ workspace 内) | thiserror 推奨 |
後で公開する可能性 / テストでエラー種別を assert したい |
「anyhow を使ったコードを公開ライブラリにする」のが一番こじれます。利用者は anyhow::Error を見せられても何もできません。最初から thiserror で型を切るほうが、後で楽になります。
エラー設計のチェックリスト
公開ライブラリのエラー enum を書くとき、以下を確認します。
- [ ]
#[derive(Debug, thiserror::Error)]が付いている - [ ]
#[non_exhaustive]が付いている - [ ] 各 variant に
#[error("...")]で人間向けメッセージがある - [ ] 内部から伝搬したいエラーに
#[from]を付けている - [ ] エラーメッセージに値が含まれる場合、placeholder で書式指定している
- [ ]
Result<T, MyError>を返す関数のドキュメントに、どの variant がいつ発生するか書いてある
最後の項目はドキュメント側ですが、利用者にとっては最も大事です。「Foo::do_it は MyError::Empty を返すことがあります」と書いておくと、利用者は match で網羅できます。
まとめ
- ライブラリのエラー型は
thiserror::Errorを#[derive]する #[error("...")]でDisplay出力を、#[from]でFrom実装を自動生成#[non_exhaustive]を付けると、利用者の match に_arm が強制され、 minor バージョンで variant を追加できる- アプリ層は
anyhowで型消去するのが楽。公開ライブラリはthiserror
thiserror + #[non_exhaustive] の2つは、新規ライブラリ初日に入れる組み合わせとしてほぼ正解です。
関連
thiserrorクレート: https://docs.rs/thiserror/#[non_exhaustive](Rust reference): https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute