m13-domain-error について
Rust 開発者向けのエラー設計スキルです。対象者、回復可能性、デバッグに必要な情報に基づくエラー分類と、適切な回復方法を示します。
主な機能
- 01対象者と回復方法によるエラー分類
- 02一時的なエラーと永続的なエラーの区別
- 03リトライ、フォールバック、タイムアウトの例
- 04診断情報と安全なメッセージの重視
活用シーン
- 01アプリケーションのエラー型の設計
- 02サービス障害時のリトライ方針の検討
- 03ログとユーザー向けエラーの改善
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ actionbook/ rust-skills --skill m13-domain-errorインストール後、Claude Code で/m13-domain-errorを入力して読み込まれたか確認します。
詳細
ドメインエラー戦略
レイヤー 2:設計上の選択
中心となる問い
このエラーに対処する必要があるのは誰で、どのように復旧すべきでしょうか?
エラー型を設計する前に、次の点を確認します。
- ユーザー向けですか、それとも内部向けですか?
- 復旧は可能ですか?
- デバッグにはどのようなコンテキストが必要ですか?
エラーの分類
| エラーの種類 | 対象 | 復旧方法 | 例 |
|---|---|---|---|
| ユーザー向け | エンドユーザー | 対応を案内する | InvalidEmail、NotFound |
| 内部向け | 開発者 | デバッグ情報を提供する | DatabaseError、ParseError |
| システム | 運用/SRE | 監視/アラート | ConnectionTimeout、RateLimited |
| 一時的 | 自動化処理 | 再試行 | NetworkError、ServiceUnavailable |
| 恒久的 | 人間 | 調査 | ConfigInvalid、DataCorrupted |
検討のための問い
エラー型を設計する前に、次の点を確認します。
-
このエラーを誰が見ますか?
- エンドユーザー → 親しみやすく、対応につながるメッセージ
- 開発者 → 詳細で、デバッグ可能な情報
- 運用担当者 → 構造化され、アラート可能な情報
-
復旧できますか?
- 一時的 → バックオフを伴う再試行
- 劣化可能 → フォールバック値
- 恒久的 → 速やかに失敗し、アラートを出す
-
どのようなコンテキストが必要ですか?
- 呼び出しチェーン → anyhow::Context
- リクエスト ID → 構造化ログ
- 入力データ → エラーペイロード
上位への追跡 ↑
ドメインの制約へ(レイヤー 3):
"How should I handle payment failures?"
↑ Ask: What are the business rules for retries?
↑ Check: domain-fintech (transaction requirements)
↑ Check: SLA (availability requirements)
| 質問 | 追跡先 | 確認事項 |
|---|---|---|
| 再試行ポリシー | domain-* | 再試行時に許容できるレイテンシはどの程度ですか? |
| ユーザー体験 | domain-* | ユーザーにはどのようなメッセージを表示すべきですか? |
| コンプライアンス | domain-* | 監査のために何をログに記録する必要がありますか? |
下位への追跡 ↓
実装へ(レイヤー 1):
"Need typed errors"
↓ m06-error-handling: thiserror for library
↓ m04-zero-cost: Error enum design
"Need error context"
↓ m06-error-handling: anyhow::Context
↓ Logging: tracing with fields
"Need retry logic"
↓ m07-concurrency: async retry patterns
↓ Crates: tokio-retry, backoff
クイックリファレンス
| 復旧パターン | 条件 | 実装 |
|---|---|---|
| 再試行 | 一時的な障害 | 指数バックオフ |
| フォールバック | 劣化モード | キャッシュ値/デフォルト値 |
| サーキットブレーカー | 障害の連鎖 | failsafe-rs |
| タイムアウト | 処理に時間がかかる場合 | tokio::time::timeout |
| バルクヘッド | 分離 | 個別のスレッドプール |
エラー階層
#[derive(thiserror::Error, Debug)]
pub enum AppError {
// User-facing
#[error("Invalid input: {0}")]
Validation(String),
// Transient (retryable)
#[error("Service temporarily unavailable")]
ServiceUnavailable(#[source] reqwest::Error),
// Internal (log details, show generic)
#[error("Internal error")]
Internal(#[source] anyhow::Error),
}
impl AppError {
pub fn is_retryable(&self) -> bool {
matches!(self, Self::ServiceUnavailable(_))
}
}
再試行パターン
use tokio_retry::{Retry, strategy::ExponentialBackoff};
async fn with_retry<F, T, E>(f: F) -> Result<T, E>
where
F: Fn() -> impl Future<Output = Result<T, E>>,
E: std::fmt::Debug,
{
let strategy = ExponentialBackoff::from_millis(100)
.max_delay(Duration::from_secs(10))
.take(5);
Retry::spawn(strategy, || f()).await
}
よくある間違い
| 間違い | 問題点 | 改善策 |
|---|---|---|
| すべて同じエラーにする | 対応につながらない | 対象に応じて分類する |
| すべてを再試行する | リソースの浪費 | 一時的なエラーのみ再試行する |
| 無限に再試行する | 自己 DoS を招く | 最大試行回数とバックオフを設定する |
| 内部エラーを公開する | セキュリティリスク | ユーザーにわかりやすいメッセージを表示する |
| コンテキストがない | デバッグが困難 | あらゆる箇所で .context() を使う |
アンチパターン
| アンチパターン | 問題点 | 改善策 |
|---|---|---|
| 文字列エラー | 構造がない | thiserror の型を使う |
| 回復可能なケースで panic! を使う | UX が悪い | コンテキストを含む Result を使う |
| エラーを無視する | 失敗が表面化しない | ログに記録するか、伝播させる |
| あらゆる箇所で Box を使う | 型情報が失われる | thiserror を使う |
| 正常系の処理でエラーが発生する | パフォーマンスの低下 | 早期に検証する |
関連スキル
| 条件 | 参照先 |
|---|---|
| エラー処理の基本 | m06-error-handling |
| 再試行の実装 | m07-concurrency |
| ドメインモデリング | m09-domain |
| ユーザー向け API | domain-* |
コメント
Markdown 対応