第34章:例外の境界(投げる・変換する場所を決める)🚧💥
0. この章で身につけること🎯✨
- 「どこで例外を投げる?」「どこでcatchする?」を迷わず決められるようになる🙆♀️✨
- 例外が散らばってカオスになるのを防いで、利用側(UI/呼び出し側)をラクにする🧹🌈
- 「スタックトレースが消えた😭」を防いで、デバッグしやすい例外にする🔍🧠 (Microsoft Learn)
1. そもそも「例外の境界」って何?🧱🚪
例外って、放っておくとこんな風に散らかりがち👇💦
- どのメソッドにも try/catch がある🕸️
- catch してログだけ出して、何事もなかったように進む(最悪😱)
- 画面に出すメッセージが低レイヤのエラー文そのまま(ユーザー困る😵💫)
そこで登場するのが 例外の境界 🚪✨ 「ここから外へ出すときは、例外を整形して渡そうね」っていう境目のルールだよ💡
- 内側:実装の都合が強い(DB/HTTP/ファイル/ライブラリ例外など)🔧
- 外側:利用者が理解できる形(画面メッセージ、HTTPレスポンス、エラーコードなど)📣
2. 例外を“3種類”に分けて考える🧠📦
分類すると、境界を決めるのが一気にラクになるよ😊✨
A) バグ or 前提違反(=直すべき)🪲🚨
null前提なのにnullが来た- 配列の範囲外
- 本来ありえない状態(不変条件が壊れた)
👉 基本:直す対象。変に握りつぶさない🙅♀️ (境界でまとめて捕まえてログ+安全に終了、が多い)
B) 入力不正(ユーザーや外部からの不正データ)📝⚠️
- 住所が空
- 金額がマイナス
- 必須項目が未入力
👉 これは 境界(UI/API)で検証して、分かる形で返すのが基本✨ 「深い所で突然例外」は利用側が困るよね😵💫
C) 外部都合の失敗(ネットワーク/DB/他サービス)🌐⛈️
- 通信失敗
- タイムアウト
- 一時的にDBが落ちてる
👉 内側の例外をそのまま漏らすと、利用側が実装依存になっちゃう💦 だから境界で「このアプリ的には何が起きた?」に変換するのが強い✨ (Microsoft Learn)
3. 例外の境界ルール(これだけ覚えればOK)✅💖
ルール1:回復できないなら、そこでcatchしない🙅♀️
「その場で直せない・回復できない」なら、catchせず上に投げた方が安全👍 例外は「回復できる場所で扱う」のが基本だよ✨ (Microsoft Learn)
ルール2:catchしたら、握りつぶさない😱🧨
- ログだけ書いて終わり
return;して何も起きてないフリ
これはデバッグ地獄の入口🥶 「失敗したなら失敗した」を伝える設計にするのが大事💡 (Microsoft Learn)
ルール3:throw ex; は基本NG、throw; を使う🧯✨
同じ例外を上に投げ直すなら、こう👇
- ✅
throw;→ 元のスタックトレース保持 - ❌
throw ex;→ スタックトレースがリセットされて原因追跡が難しくなる
これは公式の解析ルールでも注意されてるよ🧠🔍 (Microsoft Learn)
ルール4:変換するなら「wrap(包む)」🎁🧩
低レイヤ例外を、アプリ側の意味に変換するなら👇
- 新しい例外を作って
innerExceptionに元の例外を入れる
こうすると「何が起きたか」と「根っこ」の両方が残るよ✨ (Microsoft Learn)
ルール5:境界では「利用側が欲しい情報」に揃える📣✨
- UI:ユーザーに見せる文章(短く、次の行動が分かる)🙂
- API:HTTPステータスやエラーコード(機械が扱える)🤖
- バッチ:終了コード、リトライ判定材料🧾
ルール6:「よくある条件」を例外で処理しない⚖️
例外は“例外的”なときに使うのが基本だよ🫶 普通に起きる分岐は if などで扱う方が読みやすい✨ (Microsoft Learn)
4. 「どこが境界?」よくある3レイヤで決める🗺️✨
(1) 外側の境界:UI / API / バッチの入口🚪🖥️
ここは “最後に全部まとめる場所” になりやすいよ💡
- 予期しない例外をキャッチして安全に終了
- ログを残す
- 利用者向けの形にする(メッセージ/ステータス)
(2) 真ん中:アプリケーション層(ユースケース)🎮📦
- 「入力不正」「外部失敗」など意味のある失敗にまとめる
- UIに近い表現に寄せすぎない(UIが複数あると破綻しやすい)💦
(3) 内側:インフラ(DB/HTTP/ファイル)🔌🧤
ここは “他人の例外を自分の言葉に変換する場所” になりやすい✨
HttpRequestExceptionやSqlExceptionなどを- アプリ独自の例外へ変換して上に渡す🎁
5. 実践①:外部APIの例外を「アプリ用の例外」に変換する🌐➡️📦
例:支払いAPIがタイムアウトしたら…⏳💳
- 低レイヤ:
HttpRequestException/TaskCanceledException - アプリ的には:
PaymentServiceUnavailableException(支払いサービスが使えない)
public sealed class PaymentServiceUnavailableException : Exception
{
public PaymentServiceUnavailableException(string message, Exception innerException)
: base(message, innerException) { }
}
public sealed class PaymentGatewayClient
{
private readonly HttpClient _http;
public PaymentGatewayClient(HttpClient http) => _http = http;
public async Task ChargeAsync(string orderId, decimal amount, CancellationToken ct)
{
try
{
// 例:外部API呼び出し
using var response = await _http.PostAsync(
$"https://example-payments/charge?orderId={orderId}&amount={amount}",
content: null,
cancellationToken: ct);
response.EnsureSuccessStatusCode();
}
catch (HttpRequestException ex)
{
// 低レイヤ例外 → アプリ例外へ変換(元はinnerに残す)
throw new PaymentServiceUnavailableException(
"支払いサービスに接続できませんでした。時間をおいて再試行してください。",
ex);
}
catch (TaskCanceledException ex) when (!ct.IsCancellationRequested)
{
// ユーザーキャンセルではなくタイムアウト系、みたいな判定もできる👍
throw new PaymentServiceUnavailableException(
"支払いサービスがタイムアウトしました。時間をおいて再試行してください。",
ex);
}
}
}
ポイント💡✨
- 「外部の例外型」を上に漏らさない(利用側が
HttpRequestExceptionを知る必要がなくなる)🧠 - inner に元例外を残す(原因追跡できる)🔍 (Microsoft Learn)
6. 実践②:境界でcatchして“利用側”を簡単にする🧹🙂
コンソール/デスクトップ系のイメージ:最上位でまとめる🧯🖥️
try
{
await app.RunAsync();
}
catch (PaymentServiceUnavailableException ex)
{
// ユーザー向け表示(短く・次の行動が分かる)
Console.WriteLine(ex.Message);
// ログは詳細(ex自体も渡す)
logger.LogWarning(ex, "Payment failed");
}
catch (Exception ex)
{
// 予期しない例外
logger.LogError(ex, "Unhandled exception");
Console.WriteLine("予期しないエラーが発生しました。再起動しても直らない場合は連絡してください。");
}
ここでのコツ💖
- 利用側(UI)は「例外の種類」が少ないほどラク🙂
- 深い層は「原因を作らない」「作ったら意味のある例外にして上へ」✨
7. 実践③:Webなら“グローバル例外ハンドリング”が境界の主役🌍🛡️
ASP.NET Core では、例外をアプリ全体で扱う仕組みが用意されてるよ🧰✨ たとえば例外処理ミドルウェア+ハンドラーで、例外をHTTPレスポンスに整形できる📮
さらに最新の .NET 10 では、例外ハンドラーが「処理済み」とした例外について、診断出力(ログ/メトリクス等)を抑制する挙動がデフォルトになっていて、必要ならオプションで制御できるよ🧠✨ (Microsoft Learn)
8. 「スタックトレース消えた😭」を防ぐ最重要ポイント🔍🧠
✅ rethrow は throw;
try
{
DoWork();
}
catch (Exception ex)
{
logger.LogError(ex, "failed");
throw; // 元のスタックトレースを保つ
}
✅ 例外を“意味づけ”して投げるなら wrap(innerを残す)
catch (SqlException ex)
{
throw new DataAccessException("DBアクセスに失敗しました。", ex);
}
このへんは公式でも「正しく再スローしてスタックトレースを保つ」って強く書かれてるよ📚✨ (Microsoft Learn)
9. リファクタの観点:try/catch が散ってたら、こう直す🧹🧩
症状👃💦
- あちこちに
try/catch (Exception) - どこで何をしてるか分からない
- エラー表示の責務が下の層に混ざる
改善の順番(安全にいくよ)✅🌿
- 境界を1つ決める(UI/APIの入口など)🚪
- 内側の catch は「回復できる場合だけ」に減らす🙆♀️
- 外部依存の例外は、インフラ境界でアプリ例外に変換🎁
- 最後に境界でまとめて catch → 表示/レスポンス整形📣
10. ミニ演習📝✨(手を動かすと一気に定着するよ!)
演習1:try/catch の“散らばり”を回収しよう🧲🧹
- サンプルコードで
try/catchを検索🔎 - 「回復してる?」をチェック(してないなら削除候補)👀
- “境界の1か所” に例外処理を集める🚪✨
- 動作確認(テスト or 実行)✅
合格ライン🎉
catch (Exception)が深い層から消えて、境界に寄った✨
演習2:外部依存を“アプリ例外”に変換しよう🎁🌐
- HTTP/DB/ファイルの呼び出し箇所を1つ選ぶ
- そこで起きうる例外を2〜3個ピックアップ
- アプリ側の例外型を1つ作る(例:
ExternalServiceException) - inner を入れて throw new で変換
合格ライン🎉
- 上の層が
HttpRequestExceptionを知らなくてよくなった✨
演習3:throw ex; を撲滅しよう🧯🔥
throw exを検索🔎- rethrow は
throw;に変更 - “付加情報を足したい”場合は wrap に変更
- 例外が起きたときに、スタックトレースが分かりやすいか確認🔍
11. AI活用コーナー🤖✨(安全に使うコツつき)
使いどころ①:境界候補を洗い出す🗺️
AIにこう聞くと便利だよ👇
- 「このコードの境界(UI/API/インフラ)っぽい場所を列挙して」
- 「例外を catch すべき場所と理由を3つ出して」
👉 返ってきた案は、**“回復できるか?”**で最終判断しよう🙆♀️
使いどころ②:例外の変換設計(wrapの型名案)🏷️
- 「この
HttpRequestExceptionをアプリ側の例外に変換したい。例外名候補を5つ」 - 「利用側に見せるメッセージ案を3つ(短く、次の行動が分かるように)」
使いどころ③:ログに残す情報のチェック📋🧠
- 「この例外をログに出すなら、何の情報が必要?」 (例:注文ID、ユーザーID、リクエストID…など)
12. まとめ🌈✨
- 例外の境界は「内側の都合」を「外側が扱える形」に整える“関所”🚪✨
- 回復できない場所で無理にcatchしない🙅♀️(必要なら上へ) (Microsoft Learn)
- rethrow は
throw;、変換は wrap(innerに元例外)🧠🔍 (Microsoft Learn) - Webならグローバル例外処理が特に強い武器🛡️(最新の .NET 10 では診断出力の扱いも制御ポイントあり) (Microsoft Learn)
次章は、この境界設計をさらに進めて「例外 vs Result」を整理していくよ🚦📦