第20章:インフラで例外が出たときの方針(変換ルール)🧯➡️🎁
この章が終わると、DB/HTTP/外部APIなどの「I/O層で起きた例外」を、毎回ブレずに Result に変換できるようになるよ😊✨ そして、ユーザー表示とログをキレイに分けられるようになる!🫶🧾
今日のゴール🎯✨
- ✅ インフラ例外を 「インフラエラーResult」 に変換する“型”を持つ🎁
- ✅ Transient(一時的)/ Permanent(恒久的) を判断できるようにする🌩️➡️☀️
- ✅ ログに残す粒度を決める(多すぎ/少なすぎ防止)🔎
- ✅ 「変換ルール表(マップ)」を作れる📋✨
まず結論:変換ルールの基本「7つ」🧠🧷
-
インフラ例外は、そのまま上に投げない🙅♀️ → 上の層は「例外の種類」じゃなくて「失敗の意味」を知りたいの。
-
**例外→Result変換は“1か所に集める”**📍 → いろんな所で
catchしてバラバラ変換すると、地獄化する😇 -
ユーザー向け文言に、例外メッセージを使わない🚫🧨 → 例外メッセージは内部情報・英語・不安を煽る、になりがち。
-
Resultの失敗には最低でもこれを入れる🧾
Code(固定のエラーコード)🏷️UserMessage(優しい表示文言)💬Retryable(再試行して良い?)🔁ActionHint(ユーザーに促す行動)🫶
-
タイムアウト/一時的ネット不安定は “retryable: true” が基本⏳🔁 (ただし回数・間隔は別途ガード!)
-
**キャンセルは “障害” ではなく “操作結果”**🛑🙂 → ログは Error にしないことが多い(Info/Debug寄り)
-
ログには「例外+相関ID+外部依存名+操作名」を残す🔎🧵 → 後から追えるかが全て!
成果物📄✨:変換ルール表(ミニ版)

最初はこれくらいの粒度でOKだよ😊📋
-
INFRA_HTTP_TIMEOUT⏳- Retryable: ✅
- UserMessage: 「通信が混み合ってるみたい…もう一度試してね🙏」
- ActionHint: 「少し待って再試行」
-
INFRA_HTTP_UNREACHABLE🌐- Retryable: ✅
- UserMessage: 「ネットワークに接続できないみたい…🛜」
- ActionHint: 「回線確認→再試行」
-
INFRA_HTTP_RATE_LIMIT🚦- Retryable: ✅(ただし待ち時間あり)
- UserMessage: 「アクセスが集中してるよ💦 少し待ってね」
- ActionHint: 「数秒〜数十秒待つ」
-
INFRA_DB_TRANSIENT🗄️🌩️- Retryable: ✅
- UserMessage: 「ただいま保存がうまくいかなかったよ…もう一度🙏」
-
INFRA_DB_UNAVAILABLE🗄️🛠️- Retryable: ✅/❌(状況次第)
- UserMessage: 「ただいまシステムが混み合ってるよ💦」
“変換先”のエラー型(例)🧷✨
※第14〜17章で作ったエラー型の流れを引き継ぐ感じでOKだよ😊
public sealed record InfraError(
string Code,
string UserMessage,
bool Retryable,
string ActionHint,
string? DetailForLog = null, // 内部向け(ユーザーに見せない)
int? HttpStatus = null // HTTP系なら便利(次章で活躍)
);
変換の“置き場所”はここが安定😊📍
おすすめはこのどちらか:
A) インフラアダプタの出口で変換(いちばん分かりやすい)🧱
- Repository / ApiClient の中で
try/catchしてResult.Fail(InfraError)を返す
B) 境界(アプリ層の入口/出口)に集約して変換🚪
- インフラは例外を投げてもOK(ただし最終的に境界で必ず変換)
初心者向けには Aが理解しやすいよ😊✨(まずはAでOK!)
HTTP(HttpClient)でよくある例外 → 変換ルール🌐🧯
1) タイムアウト判定の“定番”⏳
最近の .NET では、タイムアウト時に TaskCanceledException が飛び、InnerException が TimeoutException になるケースが多いよ(見分けに使える)🧠✨ (Microsoft Learn)
public static InfraError MapHttpException(Exception ex)
{
// タイムアウト
if (ex is TaskCanceledException tce && tce.InnerException is TimeoutException)
{
return new InfraError(
Code: "INFRA_HTTP_TIMEOUT",
UserMessage: "通信が混み合ってるみたい…もう一度試してね🙏⏳",
Retryable: true,
ActionHint: "少し待って再試行してね",
DetailForLog: ex.ToString()
);
}
// 手動キャンセル(ユーザー操作/キャンセルToken)
if (ex is OperationCanceledException)
{
return new InfraError(
Code: "INFRA_HTTP_CANCELED",
UserMessage: "キャンセルしたよ🙂",
Retryable: false,
ActionHint: "必要ならもう一度実行してね",
DetailForLog: ex.ToString()
);
}
// その他はひとまず通信失敗扱い
return new InfraError(
Code: "INFRA_HTTP_FAILED",
UserMessage: "通信に失敗しちゃった…電波や回線を確認してね🛜💦",
Retryable: true,
ActionHint: "回線確認→再試行",
DetailForLog: ex.ToString()
);
}
2) “相手がエラーを返した”は、StatusCode を見て分類できる🧾
EnsureSuccessStatusCode() を使うと HttpRequestException が飛ぶんだけど、最近の .NET では StatusCode プロパティで判定できるよ😊 (Microsoft Learn)
- 429(Rate Limit)→ 待ってから retry ✅ 🚦
- 503/502 → 一時障害っぽいので retry ✅ 🌩️
- 401/403 → 認証/権限の可能性(基本 retry ❌)🔐
HTTPは「リトライで直せる失敗」を先に減らすのが強い💪🔁
Microsoft.Extensions.Http.Resilience で **標準の回復戦略(リトライ等)**を入れられるよ😊
AddStandardResilienceHandler が用意されてるのがポイント✨ (Microsoft Learn)
ただし! リトライしてもダメだった最後の失敗は、今回の章どおり Resultに変換して上へ渡すのがキレイ🎁✨
DB(EF Core / SqlClient)でよくある失敗 → 変換ルール🗄️🧯
1) EF Core は「接続回復(リトライ)」の仕組みがある🔁
EF Core の “Connection Resiliency” は、一時的な失敗を検知して再試行する考え方だよ😊 (Microsoft Learn)
- まずは リトライで救う
- それでもダメなら INFRA_DB_TRANSIENT / INFRA_DB_UNAVAILABLE に変換🎁
2) SqlClient 側にも「設定可能なリトライ」機能がある🧰
SqlClient には Configurable Retry Logic があって、transient エラー番号などを使って制御できるよ🧠 (Microsoft Learn)
(初心者向けの結論) ➡️ “リトライで救う” と “最後はResult変換” をセットで覚えるのが最強✨
ログ粒度の方針(ここ超大事!)🔎🧵
.NET は ILogger で 構造化ログを前提にできるよ😊 (Microsoft Learn)
まず“残す項目”テンプレ🧾✨
CorrelationId🧵(次章以降で本格)ErrorCode🏷️(INFRA_〜)Dependency🌐/🗄️(どの外部?)Operation🧰(何をしてた?)Retryable🔁DurationMs⏱️ExceptionType🧯HttpStatus(あれば)🧾
ログレベルのざっくり基準📌
- キャンセル:Information 🛑🙂
- 一時障害(timeout/503等):Warning ⏳⚠️
- 恒久的/設定ミス/致命:Error 💥
実装の型:Resultに変換して返す🎁✨
public async Task<Result<Order>> PlaceOrderAsync(...)
{
try
{
// 例:外部API呼び出し
var res = await _client.SendAsync(...);
// 相手のエラーを例外にしたいならこれ
res.EnsureSuccessStatusCode();
// 例:DB保存
await _db.SaveChangesAsync();
return Result.Success(order);
}
catch (Exception ex)
{
var infraError = _infraErrorMapper.Map(ex, dependency: "OrderApi/SqlDb", operation: "PlaceOrder");
_logger.Log(infraError.Retryable ? LogLevel.Warning : LogLevel.Error, ex,
"Infra failure. Code={Code} Dep={Dep} Op={Op} Retryable={Retryable}",
infraError.Code, "OrderApi/SqlDb", "PlaceOrder", infraError.Retryable);
return Result.Fail<Order>(infraError);
}
}
ここでのキモはね👇 「例外の種類」→「失敗の意味(コード/再試行/行動)」に翻訳して返すこと🎁✨
ミニ演習🧪✨(DB/HTTP失敗を「表示」と「ログ」に分配しよう)
お題🎀
「購入処理」で失敗したときに👇を作ってみてね😊
InfraErrorのコードを決める🏷️UserMessageを やさしい日本語で作る💬DetailForLogに 例外全文を入れる🧯RetryableとActionHintを決める🔁🫶
ケースA:HTTPタイムアウト⏳
- 表示:不安を煽らない
- ログ:相関ID、操作名、Duration、例外全文
ケースB:DB一時障害(接続が瞬断)🌩️
- 表示:「もう一度」でOK
- ログ:DB名、コマンド種別、リトライ回数(分かるなら)
AI活用コーナー🤖✨(この章に効く使い方)
1) 例外パターンの洗い出し(漏れ防止)✅
- 「HttpClient で起こりがちな例外を列挙して、timeout/ネット不通/相手エラーに分類して」
2) 変換ルール表の叩き台を作る📋
- 「INFRA_系のエラーコード案を10個、UserMessageはやさしい日本語で」
3) ログ項目レビュー役👀
- 「このログ項目で、後から原因追跡できる?不足があれば追加案を出して」
まとめ🍵😊
- インフラ例外は Resultのインフラエラーに翻訳しよう🎁
- 変換は 1か所に集約がラク📍
- timeout/503/ネット不安定は retryable の考え方が基本🔁
- ログは 追跡できる項目セットで構造化して残す🔎🧵 (Microsoft Learn)
次の第21章では、この InfraError を HTTPステータス(400/409/500…)にどう割り当てるか🚦🌐 をやるよ😊✨