0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

R04 【掟・判例】例外設計の掟 “何も起きてない”を作るのはcatch ― 握り潰し禁止とthrow;の作法

Posted at

連載Index(読む順・公開済(リンク)はここが最新): S00_門前の誓い_総合Index

例外は「不具合が自分から名乗る瞬間」です。握り潰すと、現象が「たまに落ちる」から「何も起きてない(ことになってる)」に変わる。
そして、障害対応は“最初に疑うのが自分の記憶になる作業”を強いられます。

このページは、C#/.NET(.NET 8中心)で 例外を「検索で引ける証拠」 に変えるための掟をまとめます。
題材はシンプルに3つです。

ここで言う「層」は、プログラムのスコープ(ネスト)の話ではありません。
UI/アプリ/ドメイン/インフラのような 役割のレイヤ(責務の境界) を指しています。

ここでの層(レイヤ) 具体例 例外の扱い
UI(画面/クライアント) WinFormsのForm/イベント/表示 表示用に整形(基本は握り潰さない)
アプリ(ユースケース) 受注更新/保存処理/トランザクション 失敗を“業務の言葉”に揃える(契約の統一)
ドメイン(業務ルール) 値オブジェクト/整合性/ルール ルール違反を明確化(ドメイン例外)
インフラ(DB/外部/IO) DB/HTTP/ファイル/OS 下位例外を捕捉し、証拠を残して上位へ渡す

この記事の「例外変換」は、例えば インフラ→アプリアプリ→UI の境界で「利用側が判断できる例外」に揃える話です。

  • 握り潰し禁止(証拠の保持)
  • 再throwの作法(足跡の維持)
  • 層をまたぐときの例外変換(利用側の約束に揃える)(契約の統一)

0. このページの読み方(困ったらここから)

障害が起きて、すでにログを見に行っている場合は、この順で十分です。

  1. 例外ログの発見(Error/Exceptionの探索)
  2. 発火点の特定(スタックトレースの先頭付近の確認)
  3. 同一操作の束の回収(相関ID/操作IDがある場合の絞り込み)
  4. 前後関係の追跡(同一操作のInfo/Warnの確認)
  5. 契約違反の疑い(下位例外の漏れ/握り潰し/多重ログの確認)

「例外が出ているはずなのに何も出ない」場合は、最初に握り潰し(空catch/成功扱い)を疑います。


1. このページのゴール

このページの到達点は、次の状態です。

  • 握り潰し禁止の固定(証拠の消失防止)
  • 再throw手順の固定(throw;の統一)
  • 層をまたぐときの例外変換(利用側の約束に揃える)方針の固定(上位へ渡す約束の統一)
  • ログ位置の整理(多重ログの抑止)
  • レビュー観点への分解(機械的な指摘の可能化)

2. 似ているが別物(混ぜると詰む3つ)

よく混ざるのは、次の3つです。

  • 握り潰し(失敗の成功扱い)
  • 再throw(例外の上位伝搬)
  • 層をまたぐときの例外変換(利用側の約束に揃える)(契約の統一)

この3つが混ざると「どこで、何が、なぜ消えたか」が追えなくなります。


3. 掟(要点)

  • 握り潰し禁止(少なくとも証拠の保持)
  • 再throwはthrow;(足跡の維持)
  • 層をまたぐときの例外変換(利用側の約束に揃える)(上位へ渡す約束の統一)

掟1: catchで例外を握り潰さない

なぜ効くのか

握り潰しは再現不能の入口です。
現象が「たまに失敗する」から「何も起きていない」に変わり、さらに危険な「成功扱いの仕様」へ化けます。
失敗の証人(例外)を消すと、調査の入口が閉じます。

実装の要点

  • 空catchの禁止
  • 証拠の残置(ログ/相関ID/入力/状態)
  • 復旧不能時の上位伝搬(再throw/言い換え)

失敗例(握り潰し)

try
{
    DoWork();
}
catch
{
    // 証拠ゼロ、成功扱い
}

最小の形(証拠を残して流す)

try
{
    DoWork();
}
catch (Exception ex)
{
    _logger.LogError(ex, "DoWork failed: {JobId}", jobId);
    throw; // 再throw、足跡の維持
}

掟2: 再throwはthrow;(足跡の維持)

足跡(スタックトレース)の意味

例外には「どのメソッドを経由してここまで来たか」という呼び出し履歴(スタックトレース)が付いています。
この履歴が残っていると、原因箇所(最初に落ちた地点)へ最短で辿れます。

throw ex; は、catch地点で“新しく投げた”形になるため、履歴の起点がcatch側へ寄ります。
その結果、調査がcatch付近で行き止まりになりやすいです。

ここが誤解されやすい点

throw; は「新しい例外の作成」ではありません。
同じ例外を、そのまま上位へ再送出します(情報の取りこぼしは発生しません)。

「契約を変える」必要がある場合だけ、例外型を作り直します。
そのときは 元の例外をInnerExceptionとして保持します。

使い分け

  • 原因の保持(例外型を変えない): throw;
  • 契約の変更(例外型を変える): throw new XxxException("...", ex);

例(OK/NG)

catch (Exception ex)
{
    _logger.LogError(ex, "UseCase failed");
    throw; // OK: 同一例外の再送出、足跡の維持
}
catch (Exception ex)
{
    _logger.LogError(ex, "UseCase failed");
    throw ex; // NG: 起点の寄り、発火点の追跡困難
}

掟3: 層をまたぐときに「利用側の約束」に揃える(上位へ渡す約束の統一)

なぜ効くのか

層をまたぐときに例外の意味を「利用側の約束」に揃えると、上位は下位の都合を知らずに済みます。
取り決めがないまま下位例外(ライブラリ例外)がUIまで漏れると、UIが「DB/HTTP/ファイル」などの都合で分岐し始めます。
表示文言、リトライ方針、問い合わせ導線が散らばり、呼び出し側が保守不能になりやすいです。

下位例外(ライブラリ例外)の代表例

分類 例外型(例) よくある場面
DB SqlException / DbException 制約違反、接続断、タイムアウト
HTTP/通信 HttpRequestException / SocketException 接続失敗、DNS、プロキシ
タイムアウト/キャンセル TaskCanceledException タイムアウト、Cancel
IO IOException / FileNotFoundException 欠損、共有違反
権限 UnauthorizedAccessException ACL/UAC
形式不正 FormatException / JsonException 入力、設定、JSON破損
破棄済み ObjectDisposedException Dispose後アクセス
状態不整合 InvalidOperationException 呼び出し順、状態破綻

層ごとの基本方針(ログと契約)

例外の扱い 目的
インフラ 下位例外の発生源ログ化 証拠の保持
アプリ 上位契約への言い換え 契約の統一
UI 表示向け整形 体験の保護

実装例(発生源で証拠、境界で言い換え)

try
{
    return await _repo.LoadAsync(id, ct);
}
catch (SqlException ex)
{
    _logger.LogError(ex, "DB failed: {Id}", id);
    throw new InfrastructureException("DB access failed", ex); // 境界での契約統一
}

実装例(アプリで業務寄りへ寄せる)

public async Task<Customer> LoadCustomerAsync(CustomerId id, CancellationToken ct)
{
    try
    {
        return await _repo.LoadAsync(id, ct);
    }
    catch (InfrastructureException ex)
    {
        throw new UseCaseException($"顧客情報の取得に失敗しました: {id}", ex); // 上位向け契約
    }
}

UIでの扱い(多重ログの回避)

  • 例外ログの境界集約(同一例外の多重ログ回避)
  • UI表示の最小化(詳細追跡のログ側寄せ)
try
{
    await _useCase.LoadCustomerAsync(id, ct);
}
catch (UseCaseException)
{
    ShowError("処理に失敗しました。再試行しても改善しない場合は問い合わせが必要です。");
}

どこでcatchするか(採用判断基準)

  • その場で復旧可能: 捕捉+復旧+証拠
  • 契約統一が必要: 境界で言い換え+証拠
  • 契約変更なし: 証拠+throw;
  • 成功扱いへの変換: 原則禁止(必要なら根拠と代替証拠経路)

catchを総点検する羽目になる典型ケース(業務で起きる)

検索で辿り着きやすい言い回しを、症状として並べます。

検索で来る状況(症状) よくある原因 まず見る所
catchしているのにログが出ない 空catch、成功扱いreturn catchブロック、return/continue
エラーが出ているはずなのに画面は平気 握り潰し、UI状態だけ更新 catch内の分岐、成功扱い
ログはあるが原因箇所が分からない throw ex;混入 再throw箇所
端末や環境によってだけ落ちる 下位例外の漏れ、契約不在 境界の言い換え箇所
似たログが大量に出る 多層LogError、多重ログ ログ位置、例外の同一性

判例(OK/NG)

観点 OK例 NG例 事故の形 レビュー視点
握り潰し LogError + throw; catch { } 証拠消失、再現不能 空catchの有無
再throw throw; throw ex; 発火点追跡困難 再throwの書式
境界 契約例外への言い換え 下位例外の漏れ 上位の下位依存 UIまでの例外型
ログ位置 境界で1回 多層で連発 ノイズ埋没 LogErrorの重複
後始末 finally/復旧 状態復旧なし UI破綻継続 例外経路の復旧

容赦なき断罪: レビュー観点

観点 ありがちな見落とし 事故の形 指摘の方向性
証拠 空catch、情報不足 調査入口の消失 証拠経路の追加
履歴 throw ex;混入 発火点不明 throw;統一
契約 下位例外の漏れ UI分岐爆発 境界で言い換え
多重ログ 層ごとLogError ノイズ埋没 境界集約
後始末 例外時復旧なし 状態破綻 finally/using追加

禁書庫A: 逆引き(症状→原因→対策)

症状 ありがちな原因 切り分け 最短の対処 再発防止
ログが残らず調査が止まる 空catch、未ログ catch確認 LogError+throw 空catch禁止
発火点が不明 throw ex; 再throw確認 throw;修正 ルール化
UIに原因が見えない 契約不在 UI直下catch 契約例外化 境界統一
例外ログが多すぎる 多重ログ 例外同一性 境界集約 運用固定
例外後に画面が壊れる 復旧不足 状態確認 finally復旧 観点固定

禁書庫B: チートシート

  • 最初に見る地点(最初の発火点)
  • ログが無い疑い(握り潰し/成功扱い)
  • 例外型漏れ疑い(契約不在)
  • 多重ログ疑い(境界未整理)
  • 後始末不足疑い(finally/using不足)
.NET Framework 4.8で差が出やすい点(実務上有意な場合のみ)
  • 例外のシリアライズ運用が絡む場合の注意(別プロセス/旧連携)
    • [Serializable]、シリアライズ用コンストラクタ
  • DB例外型の差分(利用クライアント差)
    • .NET Framework 4.8: System.Data.SqlClient
    • .NET 8: Microsoft.Data.SqlClient
    • 捕捉対象の実ライブラリ合わせ

推奨タグ(最大5)

  • C#
  • .NET
  • 例外設計
  • 障害対応
  • レビュー
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?