1
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;の作法

1
Last updated at Posted at 2026-01-15

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

例外は、失敗が自分で名乗り出てくれる存在。
出てくるたびに迷惑だが、原因へ辿るための手掛かりも一緒に置いていく。
catchで握り潰すと、その手掛かりだけが消え、「たまに落ちる」は「何も起きてないことになってる」へすり替わる。


このページの狙いは、例外を 「検索で引ける証拠」へ揃える こと。
掟は3つだけ。

  • 握り潰さない(証拠が消えない)
  • 再throwは throw;(足跡が残る)
  • 層をまたぐときは契約へ揃える(上位が判断できる)

0. 30秒で結論

例外設計が崩れる混入点はだいたい次の3つ。

  • 空catch / 成功扱いreturn で 証拠が消える
  • throw ex; 混入で 発火点が見えにくくなる
  • 下位例外がUIまで漏れ、UIが 下位都合で分岐し始める

止血の基本セットはこれ。

  • 発生源で証拠を残す(例外+入力+相関ID+状態)
  • 再throwは throw; へ揃える
  • 境界で契約へ揃えるnew XxxException("...", ex)InnerException 保持)

1. このページで言う「層」と「契約」

ここで言う「層」はスコープ(ネスト)ではなく、UI/アプリ/ドメイン/インフラのような 役割の境界 を指す。
また、このページでの「契約」は「上位が判断できる例外の形を揃えること」を指す。

具体例 例外の扱い(方針)
UI(画面/クライアント) WinFormsのForm/イベント/表示 表示は要約へ寄せる。証拠は消さない
アプリ(ユースケース) 受注更新/保存/トランザクション 失敗を“業務の言葉”へ揃える(契約)
ドメイン(業務ルール) 値オブジェクト/整合性/ルール ルール違反を明確化する(ドメイン例外)
インフラ(DB/外部/IO) DB/HTTP/ファイル/OS 発生源で証拠を残し、契約へ揃えて上位へ渡す

2. 見え方で切り分ける(最初に疑う混入点)

原因探しより先に「見え方」から混入点を決めると短くなる。

見え方 具体例 最初に疑う混入点
失敗しているはずなのにログが無い 問い合わせだけ来る、画面は平気 空catch、成功扱いreturn
ログはあるが発火点が見えにくい catch行付近で止まる throw ex; 混入
UIが下位都合で分岐している DB/HTTP種別で分岐が増える 契約不在(下位例外漏れ)
似た例外ログが大量に出る 同じ例外が多層でLogError 多重ログ(ログ位置未整理)
例外後に状態が崩れる Busy解除されない、操作不能 例外経路の後始末不足

3. 最短手順(困ったらこの順)

3-1. 事実を揃える

推測より先に、比較できる事実を揃える。

  • 例外(型/メッセージ/InnerException)
  • 発火点(スタックトレースの先頭付近)
  • 相関ID/操作ID(あれば)
  • 入力と状態(ID、件数、画面状態など)
  • 同一操作の前後ログ(Info/Warn)

3-2. 型へ落とす

次のどれかへ寄せると、見る場所と直し方が決まる。

  • 証拠が消えた(握り潰し)
  • 足跡が切れた(throw ex;
  • 契約が無い(下位例外漏れ)
  • ログが多い(多重ログ)
  • 状態が戻らない(後始末不足)

4. 掟1: 握り潰さない(証拠が消えない)

4-1. 失敗例(空catch)

狙い: 失敗が「何も起きてない」へ寄る形を可視化する。
読み方: catch内に証拠が無く、呼び出し側は成功扱いへ寄りやすい。

try
{
    DoWork();
}
catch
{
    // 混入点: 証拠ゼロ。失敗が見えない形へ寄る。
    // その場は静かになるが、後で切り分けが重くなりやすい。
}

4-2. 最小の形(証拠+上位へ渡す)

狙い: 例外を「検索で引ける証拠」へ寄せる。
読み方: LogErrorで例外(スタックトレース含む)を残し、throw; で足跡を維持する。

try
{
    DoWork(jobId);
}
catch (Exception ex)
{
    // ★ 例外そのものを残す(スタックトレースが証拠になる)
    _logger.LogError(ex, "DoWork failed: {JobId}", jobId);

    // ★ 同一例外を上位へ渡す(足跡を維持)
    throw;
}

4-3. 例外経路の後始末(状態が戻る形へ寄せる)

狙い: 成功/失敗どちらでも状態が揃う形へ寄せる。
読み方: 後始末は finally へ寄せると、例外経路の抜けが減る。

StartBusy();
try
{
    await DoWorkAsync(ct);
}
catch (Exception ex)
{
    _logger.LogError(ex, "DoWorkAsync failed");
    throw;
}
finally
{
    // ★ 例外時にも必ず戻す(状態崩れを避ける)
    EndBusy();
}

5. 掟2: 再throwは throw;(足跡が残る)

5-1. NG: throw ex;(発火点が寄る)

狙い: 足跡が薄くなる混入点を明確化する。
読み方: catch地点が起点へ寄り、発火点追跡が難しくなりやすい。

catch (Exception ex)
{
    _logger.LogError(ex, "UseCase failed");

    // 混入点: ここで新しく投げた形へ寄りやすい
    throw ex;
}

5-2. OK: throw;(同一例外を上位へ渡す)

狙い: 足跡(スタックトレース)を維持する。
読み方: 例外は作り直さず、そのまま上位へ渡す。

catch (Exception ex)
{
    _logger.LogError(ex, "UseCase failed");

    // ★ 同一例外を上位へ渡す(足跡を維持)
    throw;
}

6. 掟3: 層をまたぐときは契約へ揃える

6-1. 何が起きるか

下位例外(DB/HTTP/IO)がUIまで漏れると、UIが下位都合で分岐し始める。
表示文言やリトライ方針が散り、設計のまとまりが崩れやすい。

6-2. 発生源で証拠、境界で契約へ揃える

狙い: 下位都合を境界で止め、上位が判断できる形へ寄せる。
読み方: InnerException に元例外を保持し、境界で例外型を揃える。

try
{
    return await _repo.LoadAsync(id, ct);
}
catch (SqlException ex)
{
    // ★ 発生源で証拠を残す(入力も一緒に残すと追いやすい)
    _logger.LogError(ex, "DB failed: {Id}", id);

    // ★ 契約へ揃えて上位へ渡す(上位はSqlExceptionへ依存しない)
    throw new InfrastructureException("DB access failed", ex);
}

6-3. アプリで“業務の言葉”へ寄せる

狙い: UIへ届く例外を業務寄りへ揃える。
読み方: インフラ都合をUIへ漏らさず、利用側が扱える例外へ寄せる。

public async Task<Customer> LoadCustomerAsync(CustomerId id, CancellationToken ct)
{
    try
    {
        return await _repo.LoadAsync(id, ct);
    }
    catch (InfrastructureException ex)
    {
        // ★ 上位が判断できる形へ寄せる(業務の言葉へ揃える)
        throw new UseCaseException($"顧客情報の取得に失敗しました: {id}", ex);
    }
}

6-4. UIは要約へ寄せる(多重ログを避ける)

狙い: UIは体験保護へ寄せ、証拠は境界へ寄せる。
読み方: UIは例外型で分岐しすぎない形へ寄せる。

try
{
    await _useCase.LoadCustomerAsync(id, ct);
}
catch (UseCaseException)
{
    // ★ UIは要約表示へ寄せる(詳細はログで追う前提へ寄せる)
    ShowError("処理に失敗しました。再試行しても改善しない場合は問い合わせが必要です。");
}

7. 失敗パターン別:最短での見つけ方と直し方

7-1. 握り潰し(空catch / 成功扱い)

  • 見つけ方: catch { }catch { return; }catch { /*成功扱い*/ } を検索
  • 直し方: 証拠(例外+入力+相関ID)を残し、上位へ渡す

7-2. 足跡切れ(throw ex;

  • 見つけ方: throw ex; を検索
  • 直し方: throw; へ揃える(契約変更が要る場合のみ作り直す)

7-3. 契約不在(下位例外漏れ)

  • 見つけ方: UI側で SqlException / HttpRequestException など下位例外を捕まえて分岐している
  • 直し方: 境界で契約例外へ揃える(InnerException 保持)

7-4. 多重ログ(同一例外のLogError連発)

  • 見つけ方: 同一操作で LogError が多層に並ぶ
  • 直し方: LogError を境界へ寄せる(発生源で残す場合は「境界の1回」と役割を分ける)

7-5. 後始末不足(状態が戻らない)

  • 見つけ方: Busy解除、ロック解除、購読解除などが try 内に散っている
  • 直し方: finally/using へ寄せる

8. チームで揃える観点(レビュー用チェック)

  • 空catchが無い
  • catchで成功扱いreturnへ寄せていない
  • 再throwが throw; に揃っている
  • 契約例外の InnerException が保持されている
  • UIが下位例外で分岐していない
  • 例外経路で状態が戻る(finally/using)

9. 禁書庫: 現場の即効チェック

  • 「ログが無い」時、空catch/成功扱いreturn を最初に疑う
  • 「発火点が見えにくい」時、throw ex; 混入を最初に疑う
  • 「UI分岐が増えた」時、契約不在(下位例外漏れ)を最初に疑う
  • 「ログが多い」時、同一例外の多重ログを最初に疑う
  • 「状態が戻らない」時、finally/using 不足を最初に疑う

関連トピック

  • ログ設計(証拠を残す): R05

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

1
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
1
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?