例外処理の実践ルール【catchとログの判断基準】

· · 例外処理, ログ, エラーハンドリング, 設計, C# / .NET

はじめに

例外処理でありがちな問題は次の3つです。

  • 深い層で広く catch して、nullfalse を返して原因を消す
  • 各層で同じ例外を重複ログして、ログがノイズだらけになる
  • 最後の未処理例外ハンドラに回復を期待しすぎる

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)

  • HttpRequestExceptionIOException などの具体例外を受ける
  • 「支払いサービス接続失敗」「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. 1つの失敗に主ログ(Error/Critical)は1回
  2. 下位層は翻訳と文脈追加のみ。主ログを出さない
  3. 上位境界が文脈付きで主ログ
  4. 飲み込む層だけが記録責任を持つ
  5. 想定内の失敗は毎回Errorにしない
  6. OperationCanceledException は障害ログと分ける(Debug/Informationで十分)

よくある重複ログのパターン

RepositoryがError → Serviceが同じ例外をError → ControllerがまたError → 未処理ハンドラでCritical

→ 同じスタックトレースが4本並ぶ。欲しいのは1本の主ログと最小限の補助ログです。

7. C#での注意点

// スタックトレースを壊さず再スロー
throw;  // OK

throw ex;  // NG: スタックトレースがリセットされる

8. まとめ

  • 深い層は翻訳とcleanup。広くcatchしない
  • 境界は判断と主ログ。責任を持てる場所で受ける
  • ログは1回、文脈は必要なだけ
  • 例外は境界で受け、回復できる場所だけで処理する

迷ったらこの順で考える:

  1. この場所で本当に判断できるか
  2. ここで失敗単位が分かるか
  3. ここで状態を戻せるか
  4. ここでログすると重複しないか
  5. ここは回復地点か、最後の記録地点か

関連する記事

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

未想定例外発生時の対応判断フロー

想定していない例外が発生したとき、アプリを終了すべきか継続すべきかを判断するための実用的な指針をまとめた記事です。失敗単位、共有状態、外部副作用、ネイティブ境界の観点から3段階の選択肢と判断表で整理し、設計判断に直接使えます。

記事を読む

Windowsアプリのクラッシュ時ログ出力方法

Windows アプリが想定外例外で落ちても原因を追えるよう、通常ログ、最終クラッシュマーカー、WER LocalDumps、watchdog をどう組み合わせて証跡を残すかを設計の考え方とフレームワーク別の注意点まで含めて整理します。

記事を読む

Windowsアプリのセキュリティチェックリスト

WPF・WinForms・WinUI・C++・C# など Windows アプリ開発で最低限外したくないセキュリティ項目を、権限・署名・秘密情報・通信・入力・DLL・ログの観点でチェックリスト化し、リリース前に何を確認すべきか整理した記事です。

記事を読む

ユニットテストと結合テストの境界線

ユニットテストと結合テストの責務をどう切り分けるかを、判断と接続という観点で整理し、フォーマット・配線・環境・時間の四つの境界と実務で迷わない判断表、3層構成の指針までまとめた実装者向けの実践ガイドです。

記事を読む

関連トピック

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

このテーマがつながるサービス

この記事は次のサービスページにつながります。近い入口からご覧ください。

ブログ一覧に戻る