例外処理の実践ルール【catchとログの判断基準】
はじめに
例外処理でありがちな問題は次の3つです。
- 深い層で広く
catchして、nullやfalseを返して原因を消す - 各層で同じ例外を重複ログして、ログがノイズだらけになる
- 最後の未処理例外ハンドラに回復を期待しすぎる
1. まず結論
- 深い層(helper等)では広くcatchしない
- catchするのは「失敗をどう扱うか判断できる境界」
- ログは1つの失敗に1回の主ログが基本
- 一番深い層の役割は後始末と例外の翻訳。再スローするなら主ログは出さない
- 画面操作、HTTPリクエスト、ジョブ1件などの「処理境界」が主ログの適地
- 想定内の失敗は結果化(例外として投げ続けない)
2. catch・ログ・エラーハンドリングは別物
| 行為 | 意味 |
|---|---|
| catch | 例外を受け取って処理の流れを変えること。回復ではない |
| ログ | どの仕事が失敗したかを後から追える記録 |
| エラーハンドリング | 失敗の形を決めること(画面表示、HTTP応答、次件継続など) |
| 例外の翻訳 | 下位の具体的例外を、その層で意味のある失敗に変換すること |
3. 呼び出し階層ごとの役割
| 場所 | 基本方針 | ログ | 主な責務 |
|---|---|---|---|
| helper/utility | 広くcatchしない | 出さない | finallyによる後始末、局所ロールバック |
| Repository/Gateway | 具体例外だけcatch | 通常は出さない | 例外翻訳、限定的retry、接続破棄 |
| UseCase/Service | 想定内の失敗を結果化 | 飲み込むなら必要に応じて | 失敗単位の定義、部分失敗化 |
| UI/Controller/Job境界 | 想定外例外の受け口 | 主ログをここで | 応答、HTTP 500、次件継続 |
| 未処理例外ハンドラ | 漏れたものだけ | Critical | 最終記録、終了導線 |
4. 各層の具体的な役割
4.1 深い層(helper/utility)
- やってよいこと:
finallyでのリソース解放、局所ロールバック、例外メッセージへの最小限の文脈追加 - やってはいけないこと:
catch(Exception)してnull/false/空配列を返す、ここでログを出す、UIを表示する
4.2 外部I/O境界(Repository/Gateway)
HttpRequestExceptionやIOExceptionなどの具体例外を受ける- 「支払いサービス接続失敗」「CSV形式不正」などの意味のある例外に翻訳して再スロー
- retryするならここで(ただし冪等性がある場合のみ)
- 再スローするなら主ログは出さない
4.3 UseCase/Application Service
- 想定内の失敗(validation、NotFound、業務ルール違反)を結果型や失敗DTOにする
- 上位に「ユースケースとしての意味」までを決めて返す
- この層でUI表示やHTTP応答本文までは組み立てない
4.4 境界(UI/Controller/Job)
- 想定外例外をまとめて受ける
- requestId、userId、orderId付きで主ログを1回出す
- エラーダイアログ、HTTP 500、ジョブ失敗などへ変換
4.5 未処理例外ハンドラ
- 最終ログ、flush、dump収集、終了コード設定
- 回復を期待しない。ここまで来た時点で設計漏れ
- WPFの
DispatcherUnhandledExceptionなどは、Handled = trueで続行できるが、安全とは限らない
5. 失敗の種類を分ける
| 失敗の種類 | 扱う場所 | 典型的な扱い |
|---|---|---|
| validationエラー | UseCase/境界 | 入力エラーとして返す |
| NotFound/Conflict | UseCase/Controller | 404/409など |
| ユーザーキャンセル | 操作境界 | キャンセル扱い。Errorにしない |
| CSV1行不正 | 1行境界 | Warningで記録し次へ |
| 一時的timeoutで最終失敗 | I/O〜request境界 | retry後に失敗として返す |
| NullReferenceException | request/job境界 | 主ログして失敗応答 |
| AccessViolationException等 | 最終境界 | Critical、終了寄り |
6. ログの鉄則
- 1つの失敗に主ログ(Error/Critical)は1回
- 下位層は翻訳と文脈追加のみ。主ログを出さない
- 上位境界が文脈付きで主ログ
- 飲み込む層だけが記録責任を持つ
- 想定内の失敗は毎回Errorにしない
OperationCanceledExceptionは障害ログと分ける(Debug/Informationで十分)
よくある重複ログのパターン
RepositoryがError → Serviceが同じ例外をError → ControllerがまたError → 未処理ハンドラでCritical
→ 同じスタックトレースが4本並ぶ。欲しいのは1本の主ログと最小限の補助ログです。
7. C#での注意点
// スタックトレースを壊さず再スロー
throw; // OK
throw ex; // NG: スタックトレースがリセットされる
8. まとめ
- 深い層は翻訳とcleanup。広くcatchしない
- 境界は判断と主ログ。責任を持てる場所で受ける
- ログは1回、文脈は必要なだけ
- 例外は境界で受け、回復できる場所だけで処理する
迷ったらこの順で考える:
- この場所で本当に判断できるか
- ここで失敗単位が分かるか
- ここで状態を戻せるか
- ここでログすると重複しないか
- ここは回復地点か、最後の記録地点か
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
未想定例外発生時の対応判断フロー
想定していない例外が発生したとき、アプリを終了すべきか継続すべきかを判断するための実用的な指針をまとめた記事です。失敗単位、共有状態、外部副作用、ネイティブ境界の観点から3段階の選択肢と判断表で整理し、設計判断に直接使えます。
Windowsアプリのクラッシュ時ログ出力方法
Windows アプリが想定外例外で落ちても原因を追えるよう、通常ログ、最終クラッシュマーカー、WER LocalDumps、watchdog をどう組み合わせて証跡を残すかを設計の考え方とフレームワーク別の注意点まで含めて整理します。
Windowsアプリのセキュリティチェックリスト
WPF・WinForms・WinUI・C++・C# など Windows アプリ開発で最低限外したくないセキュリティ項目を、権限・署名・秘密情報・通信・入力・DLL・ログの観点でチェックリスト化し、リリース前に何を確認すべきか整理した記事です。
ユニットテストと結合テストの境界線
ユニットテストと結合テストの責務をどう切り分けるかを、判断と接続という観点で整理し、フォーマット・配線・環境・時間の四つの境界と実務で迷わない判断表、3層構成の指針までまとめた実装者向けの実践ガイドです。
Windows待機処理はイベントを使え【15.6msポーリングの罠】
Windows の Sleep や timed wait は約15.6ms の clock 粒度に縛られ、思った精度が出ません。仕事到着や I/O 完了、停止要求は timer ポーリングではなく event 駆動で待つべき理由と、典型アンチパターンの直し方を整理します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
不具合調査・原因解析
再現しにくい障害、長期稼働後の不具合、通信停止などの調査を支援します。