| name | rust-error-handling |
| description | Rustでのエラー設計を、境界ごとに thiserror / anyhow を使い分けて実装する。ドメイン/ライブラリは型付きエラー(thiserror)、アプリ境界のみ anyow。context付与、unwrap禁止、HTTP/CLI変換の指針を含む。 |
Rust Error Handling: anyhow / thiserror の境界設計
概要
目的
- 例外的な失敗を「握りつぶさず」「原因を辿れる形」で伝搬し、境界で適切に変換する。
- ドメイン層のAPIを型付きエラーで安定させ、上位で集約・ログ化・ユーザー向け変換ができるようにする。
適用範囲
- ライブラリ/ドメイン層:
thiserrorによる型付きエラー(Result<T, Error>) - アプリケーション境界(main/CLI/HTTPハンドラ等):
anyhow::Resultと.context()/.with_context()
やらないこと
- ドメイン層の public API に
anyhow::Errorを露出しない。 - 「とりあえず
Stringエラー」で返さない(判断不能になる)。
前提となる役割分担
anyhowanyhow::Errorとanyhow::Result<T>による「型消去された汎用エラー型」。- アプリケーションコードでの「簡易なエラー統合・伝搬・コンテキスト付与」に用いる。
thiserror#[derive(Error)]でstd::error::Error実装を自動生成するためのクレート。- ライブラリ/ドメイン層での「型付きエラー定義」に用いる。
ライブラリ/ドメイン層 →
thiserrorで意味のある Error 型を定義アプリケーション境界(
mainなど) → 複数の Error をanyhowでまとめて扱う
アプリケーション層(binary crate)でのルール — anyhow
戻り値は
anyhow::Result<T>を使うのは「最上位だけ」mainや CLI ハンドラ、HTTP サーバのエントリポイントなど、
「最終的にログを出して終了/レスポンスに変換する層」に限定してanyhow::Result<()>を使う。- ドメインロジックにまで
anyhow::Resultを広げない。
use anyhow::Result; fn main() -> Result<()> { app::run()?; Ok(()) }.context()/.with_context()でエラーに文脈を必ず付ける- 「どの操作中に失敗したのか」がわかるメッセージを付ける。
use anyhow::{Context, Result}; fn load_config(path: &str) -> Result<String> { std::fs::read_to_string(path) .with_context(|| format!("failed to read config from {path}")) }「ハンドルできない/ハンドルしない」境界でのみ anyhow に集約する
- HTTP レイヤや CLI レイヤで「ログを出す」「ユーザー向けメッセージに変換する」直前で、
下位の
thiserrorベースのエラーをanyhow::Errorに吸わせるのは OK。 - それより下の層では 独自 Error 型のまま 保つ。
- HTTP レイヤや CLI レイヤで「ログを出す」「ユーザー向けメッセージに変換する」直前で、
下位の
unwrap/expectの禁止(初期化コードなど例外的ケースを除く)- ランタイムで発生しうる失敗はすべて
Result/Optionとして扱い、?とanyhow/thiserrorで処理する。
- ランタイムで発生しうる失敗はすべて
ライブラリ/ドメイン層でのルール — thiserror
Public API では
anyhowを返さず、自前の Error 型を定義するpub fn ... -> Result<T, Error>のErrorは自前の enum / struct。anyhow::Errorを public API に出すのは禁止。
use thiserror::Error; #[derive(Debug, Error)] pub enum RepositoryError { #[error("db error: {0}")] Db(#[from] sqlx::Error), #[error("entity not found: {id}")] NotFound { id: String }, } pub type Result<T> = std::result::Result<T, RepositoryError>;#[from]で外部エラーをラップし、source を保持する- 依存クレートのエラーや IO エラーは、
#[from]を使って自動変換する。 - これにより
?演算子で自然に伝搬できる。
- 依存クレートのエラーや IO エラーは、
エラー型は「使う側の判断に必要な粒度」で設計する
- 「ユーザー入力ミス」「外部サービスの障害」「内部バグ」など、 リトライ可否や HTTP ステータス変換などに必要な分類を enum variant として持たせる。
#[derive(Debug, Error)] pub enum DomainError { #[error("invalid input: {0}")] InvalidInput(String), #[error("external service failed: {0}")] External(String), #[error("unexpected internal error")] Internal(#[from] anyhow::Error), // ← ドメイン内だけで包むのはアリ }Error 型はモジュール/境界ごとに分ける
- 1 つの巨大な
Errorenum に何でも詰め込まず、 「RepositoryError」「DomainError」「ApiError」のように責務ごとに分割する。
- 1 つの巨大な
チェックリスト
- ドメイン層の public API は
Result<T, DomainError>(または責務別Error)になっている -
#[from]による source 保持ができている(原因追跡できる) - アプリ境界で
.context()/.with_context()が付与されている -
unwrap/expectが残っていない(例外: テスト、明示された初期化のみ) - HTTP/CLI変換が match で明示され、判断基準が読み取れる