コンテンツにスキップ
検索語を入力してください

    エラーハンドリング

    ドメインコードで unwrappanic に頼ると、想定内のビジネス失敗とバグの区別がつかなくなる。Kamae ではドメイン固有の enumResult で失敗を明示し、インフラエラーはアダプター境界で変換する。

    ユースケースの流れは 状態遷移アプリケーション配線 とセット。ログとソースチェーンは ロギングとメトリクスthiserror の置き方は クレートガイド(thiserror) を参照する。

    ドメイン固有のエラー enum を使う

    Section titled “ドメイン固有のエラー enum を使う”

    呼び出し元が分岐すべき失敗は列挙型のバリアントに載せ、anyhow や文字列エラーは HTTP ハンドラなど報告境界まで留める。

    #[derive(Debug, thiserror::Error)]
    pub enum AssignDriverError {
    #[error("request not found: {request_id}")]
    RequestNotFound { request_id: RequestId },
    #[error("request is not waiting")]
    InvalidState,
    #[error("driver is not available: {driver_id}")]
    DriverNotAvailable { driver_id: DriverId },
    }

    ドメイン関数から anyhow::ErrorBox<dyn Error>String を返さない。それらはエラーを報告またはログするアプリケーション境界付近では許容される。

    ドメインコードで panic を避ける

    Section titled “ドメインコードで panic を避ける”

    ドメインおよびユースケースコードでは panic!todo!unimplemented!unwrap()expect() を避ける。代わりに型付きエラーまたはテスト専用ヘルパーを使う。

    許容される例外:

    • テストとフィクスチャ
    • 網羅的ドメイン推論で守られた真に到達不能な分岐
    • クラッシュが意図された挙動であるプロセス起動時の設定失敗

    インフラエラーを意図的に変換する

    Section titled “インフラエラーを意図的に変換する”

    インフラとアプリケーションロジックの境界で、repository および adapter エラーをユースケースエラーにマップする。

    let request = repository
    .find_by_id(&request_id)
    .await
    .map_err(AssignDriverError::Repository)?;

    低レベル crate の error 型を、明示的なプロジェクト慣習でない限り、ドメインユースケースの公開エラー契約にしない。

    Rust サーバーコードでは慣用的な形は async fn -> Result<T, E> であり、Result<Future<_>, E> ではない。future は Result に解決し、async 本体で ? を使う。

    層を分離する:

    典型的な形状エラー型
    ドメイン遷移同期 fn または所有権を消費するメソッドDomainError
    ユースケースasync fn#[from] バリアントを持つ UseCaseError
    ポート / アダプタtrait 内 async fnRepositoryError, ClientError, …

    可能ならドメイン遷移は同期かつ純粋に保つ。async は I/O を伴うユースケースとアダプタに属する。

    アプリケーション配線 に正規の AssignDriver 非同期ユースケース形状がある。パターンは: port 経由でロード、純粋ドメイン遷移、別 port で persist — 各 .await サイトでインフラエラーをマップする。

    ガイドライン:

    • .await サイトで map_err または #[from] によりインフラエラーをマップする。
    • ユースケースで mutex ガードなどのロックを .await 越しに保持しない。
    • 本当に独立した port 呼び出しだけ、ユースケース層で tokio::try_join! 等を使う。
    • ハンドラ内の文字列的リトライより、ConcurrentModification のような型付きリトライ可能エラーを優先する。

    プロジェクトがすでに標準化していない限り、ResultAsync 風コンビネータは導入しない。? と層別 error enum が Kamae の Rust デフォルトである。

    #[source]#[from] でエラーを連鎖させる

    Section titled “#[source] と #[from] でエラーを連鎖させる”

    thiserror の source 連鎖を使い、呼び出し側と observability ツールが文字列連結なしで失敗経路を辿れるようにする。

    #[derive(Debug, thiserror::Error)]
    pub enum AssignDriverError {
    #[error("request not found: {request_id}")]
    RequestNotFound { request_id: RequestId },
    #[error("domain transition failed")]
    Domain(#[from] TaxiRequestError),
    #[error("persistence failed")]
    Repository(#[from] RepositoryError),
    }
    #[derive(Debug, thiserror::Error)]
    pub enum RepositoryError {
    #[error("database query failed")]
    Query(#[source] sqlx::Error),
    #[error("optimistic lock conflict on {aggregate_id}")]
    ConcurrentModification { aggregate_id: RequestId },
    }

    ガイドライン:

    • 下位層エラーは #[from] または明示バリアントで包む。唯一の文脈として format!("{e}") にしない。
    • インフラエラーを包んでも、リーフバリアントは意味論的に保つ(ConcurrentModificationRateLimited)。
    • PII をエラーメッセージや Display に付けない。ドメイン ID のみ(ロギングとメトリクス 参照)。
    • ユースケースパスごとに権威ある error 返却は 1 つ。adapter はマップし、ドメインは各層をログせず型付き enum を返す。

    構造化ログと統合するとき、操作を所有する層で一度だけエラーを記録し、{error} / %error フォーマットで全連鎖を出力する(構造化ログとエラーチェーンの統合 参照)。

    エラーメッセージは PII とシークレットを避けているか — High

    Section titled “エラーメッセージは PII とシークレットを避けているか — High”

    PII 保護 も照合する。メール、電話、トークン、生の SQL / HTTP ボディを埋め込むエラーの Display テキストを指摘する。

    ドメインとユースケースコードでパニックは避けているか — High

    Section titled “ドメインとユースケースコードでパニックは避けているか — High”

    テスト、フィクスチャ、起動コード、真に到達不能な分岐以外での panic!todo!unimplemented!unwrap()expect() を指摘する。

    起動 / 設定のフェイルファストパニック、テスト / フィクスチャのパニック、マイグレーションアサーション、同一式で既に証明された不変条件を守る expect メッセージには指摘しない。

    ロックは await 点をまたいで保持されていないか — High

    Section titled “ロックは await 点をまたいで保持されていないか — High”

    プロジェクトが明示的に設計していない限り、ユースケースやアダプタで mutex ガード、DB 行ロック、その他の排他リソースを .await をまたいで保持する箇所を指摘する。

    async ユースケースは正しく層分けされているか — Medium

    Section titled “async ユースケースは正しく層分けされているか — Medium”

    I/O を伴う async ドメイン遷移、Result<impl Future<...>, E> 型 API、マッピングなしに async fn 境界を通過するインフラエラー型を指摘する。

    インフラエラーは意図的に変換されているか — Medium

    Section titled “インフラエラーは意図的に変換されているか — Medium”

    sqlx::Errordiesel::result::Error、HTTP クライアントエラー、設定エラーを公開ドメイン / ユースケース API へそのまま漏らす箇所を指摘する。

    エラーは #[source] / #[from] でチェーンされているか — Medium

    Section titled “エラーは #[source] / #[from] でチェーンされているか — Medium”

    内部失敗を format! で文字列化し、thiserror のソースチェーンを失うユースケースエラーを指摘する。

    ドメインエラーは具体的な列挙型か — Medium

    Section titled “ドメインエラーは具体的な列挙型か — Medium”

    ドメインコンストラクタやユースケースから anyhow::Resulteyre::ResultBox<dyn Error>String、不透明な catch-all エラーを返す箇所を指摘する。

    エラーバリアントは呼び出し元にとって意味があるか — Low

    Section titled “エラーバリアントは呼び出し元にとって意味があるか — Low”

    呼び出し元が網羅的に分岐する必要があるのに Other(String)InvalidInput(String) のような曖昧なバリアントを指摘する。