メインコンテンツまでスキップ

第20章:インフラで例外が出たときの方針(変換ルール)🧯➡️🎁

この章が終わると、DB/HTTP/外部APIなどの「I/O層で起きた例外」を、毎回ブレずに Result に変換できるようになるよ😊✨ そして、ユーザー表示ログをキレイに分けられるようになる!🫶🧾


今日のゴール🎯✨

  • ✅ インフラ例外を 「インフラエラーResult」 に変換する“型”を持つ🎁
  • Transient(一時的)/ Permanent(恒久的) を判断できるようにする🌩️➡️☀️
  • ログに残す粒度を決める(多すぎ/少なすぎ防止)🔎
  • ✅ 「変換ルール表(マップ)」を作れる📋✨

まず結論:変換ルールの基本「7つ」🧠🧷

  1. インフラ例外は、そのまま上に投げない🙅‍♀️ → 上の層は「例外の種類」じゃなくて「失敗の意味」を知りたいの。

  2. **例外→Result変換は“1か所に集める”**📍 → いろんな所で catch してバラバラ変換すると、地獄化する😇

  3. ユーザー向け文言に、例外メッセージを使わない🚫🧨 → 例外メッセージは内部情報・英語・不安を煽る、になりがち。

  4. Resultの失敗には最低でもこれを入れる🧾

  • Code(固定のエラーコード)🏷️
  • UserMessage(優しい表示文言)💬
  • Retryable(再試行して良い?)🔁
  • ActionHint(ユーザーに促す行動)🫶
  1. タイムアウト/一時的ネット不安定は “retryable: true” が基本⏳🔁 (ただし回数・間隔は別途ガード!)

  2. **キャンセルは “障害” ではなく “操作結果”**🛑🙂 → ログは Error にしないことが多い(Info/Debug寄り)

  3. ログには「例外+相関ID+外部依存名+操作名」を残す🔎🧵 → 後から追えるかが全て!


成果物📄✨:変換ルール表(ミニ版)

Translation Map

最初はこれくらいの粒度で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失敗を「表示」と「ログ」に分配しよう)

お題🎀

「購入処理」で失敗したときに👇を作ってみてね😊

  1. InfraError のコードを決める🏷️
  2. UserMessageやさしい日本語で作る💬
  3. DetailForLog例外全文を入れる🧯
  4. RetryableActionHint を決める🔁🫶

ケース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章では、この InfraErrorHTTPステータス(400/409/500…)にどう割り当てるか🚦🌐 をやるよ😊✨