連載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