rust-error-handling
skillRustでのエラー設計を、境界ごとに thiserror / anyhow を使い分けて実装する。ドメイン/ライブラリは型付きエラー(thiserror)、アプリ境界のみ anyhow。context付与、unwrap禁止、HTTP/CLI変換、Clone運用の指針を含む。
apm::install
apm install @see2et/rust-error-handlingapm::skill.md
---
name: rust-error-handling
description: Rustでのエラー設計を、境界ごとに thiserror / anyhow を使い分けて実装する。ドメイン/ライブラリは型付きエラー(thiserror)、アプリ境界のみ anyhow。context付与、unwrap禁止、HTTP/CLI変換、Clone運用の指針を含む。
---
# Rust Error Handling: anyhow / thiserror の境界設計
## 概要
### 目的
- 例外的な失敗を「握りつぶさず」「原因を辿れる形」で伝搬し、境界で適切に変換する。
- ドメイン層のAPIを型付きエラーで安定させ、上位で集約・ログ化・ユーザー向け変換ができるようにする。
### 適用範囲
- **ライブラリ/ドメイン層**: `thiserror` による型付きエラー(`Result<T, Error>`)
- **アプリケーション境界(main/CLI/HTTPハンドラ等)**: `anyhow::Result` と `.context()` / `.with_context()`
### やらないこと
- ドメイン層の public API に `anyhow::Error` を露出しない。
- 「とりあえず `String` エラー」で返さない(判断不能になる)。
## 前提となる役割分担
- **`anyhow`**
- `anyhow::Error` と `anyhow::Result<T>` による「型消去された汎用エラー型」。
- **アプリケーションコード**での「簡易なエラー統合・伝搬・コンテキスト付与」に用いる。
- **`thiserror`**
- `#[derive(Error)]` で `std::error::Error` 実装を自動生成するためのクレート。
- **ライブラリ/ドメイン層**での「型付きエラー定義」に用いる。
- **ライブラリ/ドメイン層** → `thiserror` で意味のある Error 型を定義
- **アプリケーション境界(`main` など)** → 複数の Error を `anyhow` でまとめて扱う
## アプリケーション層(binary crate)でのルール — anyhow
1. **戻り値は `anyhow::Result<T>` を使うのは「最上位だけ」**
- `main` や CLI ハンドラ、HTTP サーバのエントリポイントなど、
「最終的にログを出して終了/レスポンスに変換する層」に限定して `anyhow::Result<()>` を使う。
- ドメインロジックにまで `anyhow::Result` を広げない。
```rust
use anyhow::Result;
fn main() -> Result<()> {
app::run()?;
Ok(())
}
```
2. **`.context()` / `.with_context()` でエラーに文脈を必ず付ける**
- 「どの操作中に失敗したのか」がわかるメッセージを付ける。
```rust
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}"))
}
```
3. **「ハンドルできない/ハンドルしない」境界でのみ anyhow に集約する**
- HTTP レイヤや CLI レイヤで「ログを出す」「ユーザー向けメッセージに変換する」直前で、
下位の `thiserror` ベースのエラーを `anyhow::Error` に吸わせるのは OK。
- それより下の層では **独自 Error 型のまま** 保つ。
4. **`unwrap` / `expect` の禁止(初期化コードなど例外的ケースを除く)**
- ランタイムで発生しうる失敗はすべて `Result` / `Option` として扱い、`?` と `anyhow` / `thiserror` で処理する。
## ライブラリ/ドメイン層でのルール — thiserror
1. **Public API では `anyhow` を返さず、自前の Error 型を定義する**
- `pub fn ... -> Result<T, Error>` の `Error` は自前の enum / struct。
- `anyhow::Error` を public API に出すのは禁止。
```rust
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>;
```
2. **`#[from]` で外部エラーをラップし、source を保持する**
- 依存クレートのエラーや IO エラーは、`#[from]` を使って自動変換する。
- これにより `?` 演算子で自然に伝搬できる。
3. **エラー型は「使う側の判断に必要な粒度」で設計する**
- 「ユーザー入力ミス」「外部サービスの障害」「内部バグ」など、
リトライ可否や HTTP ステータス変換などに必要な分類を enum variant として持たせる。
```rust
#[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), // ← ドメイン内だけで包むのはアリ
}
```
4. **Error 型はモジュール/境界ごとに分ける**
- 1 つの巨大な `Error` enum に何でも詰め込まず、
「RepositoryError」「DomainError」「ApiError」のように責務ごとに分割する。
5. **`thiserror` エラーの `Clone` は条件付きで採用する**
- 上位レイヤで「最後のエラー保持」「再利用」「再送」などが必要で、
かつ内包するエラー型が `Clone` を実装済みなら `#[derive(Clone, Debug, Error)]` を採用する。
- `Clone` が不要なエラー型にはむやみに付けない。
6. **`Option<Error>` の状態保持では参照アクセサを併設する**
- `Option<Error>` をキャッシュする状態型では、
不要な clone を避けるため `Option<&Error>` を返す参照アクセサ(例: `last_ref()`)を用意する。
- 文字列化やログ出力など「参照で足りる」処理は参照アクセサを使う。
- 既存 API 互換が必要な場合のみ、所有権を返すアクセサ(例: `last()`)を併存させる。
## チェックリスト
- [ ] ドメイン層の public API は `Result<T, DomainError>`(または責務別Error)になっている
- [ ] `#[from]` による source 保持ができている(原因追跡できる)
- [ ] アプリ境界で `.context()` / `.with_context()` が付与されている
- [ ] `unwrap/expect` が残っていない(例外: テスト、明示された初期化のみ)
- [ ] HTTP/CLI変換が match で明示され、判断基準が読み取れる
- [ ] `Clone` が必要な `thiserror` エラーだけに `Clone` derive を付与している
- [ ] `Option<Error>` の状態保持型で、参照アクセサ(例: `last_ref()`)を優先して不要 clone を避けている