アプリケーションアーキテクチャ

エラーハンドリング ― 落ちても復旧できるシステム ― 生成AI時代のアーキテクチャ超入門

エラーハンドリング ― 落ちても復旧できるシステム ― 生成AI時代のアーキテクチャ超入門

本記事について

当サイトを閲覧いただきありがとうございます。 本記事はシリーズ『生成AI時代のアーキテクチャ超入門』の「アプリケーションアーキテクチャ」カテゴリ最終記事(第4弾)として、エラーハンドリングについて解説する記事です。

正常系はどの実装でも似通いますが、違いが出るのは「DBが一瞬落ちた」「外部APIが遅延した」「入力が想定外だった」時の振る舞いです。本記事ではエラー分類・例外vsResult型・相関ID・リトライ戦略・冪等性・Circuit Breakerなど、「落ちても復旧できるシステム」の設計指針を示します。

本記事のテーマについてさらに詳しく知りたい方は『セキュア・バイ・デザイン』も参考にしてみてください。

この記事の結論

  • エラーを「予期する / しない」で分類し、扱いを変える
  • 外部呼び出しはリトライ+冪等性+Circuit Breakerをセットで設計する
  • タイムアウト無指定は時限爆弾。全ての外部呼び出しに必ず設定する
  • 異常系の想像はAIに任せず人間が行い、AIには型で縛りをかける

この記事を読む前に

本記事はプログラムの書き方・整理の仕方の話が中心です。IT用語にあまり馴染みがない方は、基礎編の「プログラムとAPIの基本」を先に読んでおくと格段に分かりやすくなると思います。また、読んでいて分からない用語が出てきたときは用語集で調べながら読み進められます。

そもそもエラーハンドリングとは何か

エラーハンドリングとは、ざっくり言えば「プログラムが想定外の事態に遭遇したとき、どう対処するかを事前に決めておく設計」です。

車のエアバッグやABSに近い存在です。正常に走っているときは目に見えませんが、衝突やスリップが起きた瞬間に作動して被害を最小限にします。ソフトウェアでも同じで、DBが一瞬落ちた・外部APIが返事をくれない・ユーザーが想定外の値を入力した──こうした「いつか必ず起きる異常」に対して、どこで検知し、どう回復し、ユーザーに何を伝えるかを決めておくのがエラーハンドリングです。

なぜエラーハンドリングが重要なのか

もしエラーハンドリングがなかったら、些細な障害がシステム全体を巻き込みます。1つのサービスが遅延しただけで呼び出し元のスレッドが全部詰まり、そこを呼んでいた別サービスも詰まり、と雪崩式に全体が止まります。運用事故の9割は異常系の設計不備です。

「落ちないシステム」ではなく「落ちても復旧できるシステム」を作る。それがエラー設計の本質です。

エラーの分類 ― 全ての出発点

エラー設計で最初にやるべきは、発生するエラーを性質ごとに分類することです。

種類対応
プログラムバグNullPointer・型エラー修正するしかない
入力エラーバリデーション失敗ユーザーに返却して再入力を促す
業務エラー在庫不足・残高不足業務フローで処理する
一時障害ネットワーク・外部APIタイムアウトリトライで回復を試みる
恒常障害認証失敗・権限不足リトライ不可・即座に失敗させる

分類を怠ると3つの事故が起きます。ユーザーに見せてはいけない内部エラー(スタックトレース)が画面に露出する。リトライすべき一時障害とリトライしてはいけない業務エラーが区別できず二重決済のような重大事故を生む。バグと想定内の業務エラーが混ざり、アラートが鳴り続けて重要な警告が埋もれる。「共通の基底クラスで一括catchする設計」は最悪です。

例外 vs Result型

エラーをコードで表現する方法は例外方式(Java / C# / Python / JS)とResult型方式(Rust / Go)の2つがあり、対立する概念ではなくエラーの性質で使い分けるのが現代の主流です。

// Rust の Result
let value = repo.find(id)?;  // ?演算子でエラーを上に伝搬

予期できる失敗(入力エラー・業務エラー)はResult / Eitherで、呼出側に処理を強制します。予期できない失敗(バグ・DB障害・メモリ不足)は例外で上位に投げる方が安全です。全てを例外にするとどの関数が何を返すか見えなくなり、全てをResult型にすると if err != nil だらけで本質が埋もれます。「予期できる/できない」で線を引くのがバランスの良い設計です。

エラーの境界とメッセージ

エラー処理の境界設計(層ごとの責務)

エラーは「発生した場所で処理する」のではなく、「適切な境界で集約して処理する」のが原則です。最上位の境界(コントローラやAPIゲートウェイ)のグローバルエラーハンドラでまとめて捕捉し、HTTPステータスやJSONレスポンスに変換します。各階層で毎回try/catchするとコードがノイズまみれになり、握り潰しのリスクも高まります。

❌ catch (e) { /* 何もしない */ }   ← 例外握り潰し(最悪)
❌ catch (Exception e) { log(e) }   ← 全部同じ扱い(区別なし)
❌ return null / -1 で失敗を表現     ← 呼出側が気付かない

メッセージは「開発者向け」「エンドユーザー向け」で分けます。

❌ ユーザーに表示: "java.sql.SQLIntegrityConstraintException: duplicate key..."
✅ ユーザーに表示: "このメールアドレスは既に登録されています"
   ログに記録:    詳細スタックトレース + trace_id: abc123

技術的詳細をユーザーに見せると攻撃の手がかりになり、「エラーが発生しました」だけでは問い合わせ時に何も追えません。両者を繋ぐのが相関ID(Trace ID)です。リクエスト入口で付与した一意のIDを全サービスに伝播させ、エラー画面にも載せておけば、「エラー番号abc123でした」という問い合わせからログ横断で即座に経路と原因を追えます。現場の本命はOpenTelemetryで、発行・伝播・可視化まで自動化できます。後付けは辛いので最初から入れます。

外部依存の防御3点セット ― リトライ・冪等性・Circuit Breaker

リトライ戦略とJitter

一時障害は数秒後にリトライすれば成功するケースがほとんどですが、無邪気なリトライは事態を悪化させます。鉄板は「指数バックオフ+Jitter+最大試行回数」の三点セットです。間隔を1→2→4→8秒と倍々に広げ、ランダムな揺らぎ(Jitter)を加え、3〜5回で諦める。多数のクライアントが同じ瞬間にリトライすると「Thundering Herd」となり、復旧しかけた外部サービスを再び落とすため、Jitterは必須です。AWS SDK等の主要ライブラリはデフォルトでこれを実装しているので、自前実装よりライブラリ任せが鉄則です。

冪等性 ― リトライの前提条件

リトライと必ずセットで検討すべきが冪等性(同じリクエストを何度送っても結果が同じ)です。

❌ POST /users をネットワーク障害で3回リトライ
   → 同じユーザーが3人作られる

✅ POST /users + Idempotency-Key: uuid-abc123
   → 同じキーで2回目以降は最初の結果を返す

クライアントが発行するUUIDをリクエストに含め、サーバーで記録し、同じキーが来たら初回の結果を返します。金銭や副作用を伴う処理には必ず導入すべきパターンです。

Circuit Breaker・タイムアウト・Bulkhead

外部サービスが障害中に呼び出しを送り続けると、自分のスレッドプールが枯渇して共倒れします。Circuit Breakerは電気のブレーカーと同じ発想で、失敗が閾値を超えたら遮断(Open)して即座にエラーを返し、一定時間後に試験リクエストで復旧判定(Half-Open)します。実装はResilience4j / Polly / Istio等が定番です。

そして「タイムアウト未設定」は最も頻繁に見る事故原因です。HTTPクライアントやDB接続を無指定のまま使うと、相手が応答しない時にスレッドが永遠に掴まれ、やがてシステム停止へ発展します。Bulkheadは船の隔壁と同じで、接続プールを機能ごとに分離し、1つの外部サービスの遅延が全スレッドを食い尽くさないようにします。

エラーハンドリングの3層構造

どう選べばいいのか ― 段階的な実装優先度

全パターンを最初から導入すると過剰設計になります。防御の装備は段階的に足すもので、その段階は規模でほぼ決まります。

タイムアウト・リトライの数値Gate

※ 2026年4月時点の業界相場値です。

設定項目推奨値
HTTPクライアントタイムアウト接続5秒 / 読み取り30秒
DB接続タイムアウト接続3秒 / クエリ30秒
リトライ最大回数3〜5回(+Jitter 0〜1秒)
Circuit Breakerエラー率閾値50%(直近10秒)・半開復帰30〜60秒
Idempotency-Key TTL24時間

3つのシナリオで考える

個人開発・スタートアップの場合

例外とグローバルハンドラ、タイムアウトの3点があれば十分です。全ての例外を1箇所で捕まえてSentryに送って、HTTPクライアントには接続5秒・読み取り30秒のタイムアウトを設定する。たったこれだけのことですが、これで無防備な状態からは脱出できてしまいます。

個人・スタートアップ ― 1か月で出せる構成が正解 ― 生成AI時代のアーキテクチャ超入門senkohome.com/arch-intro-case-startup/

中小SaaSの場合

上記に加えて、相関IDとリトライ(Jitter付き)、冪等性を装備する段階になります。決済や外部APIとの連携が出てくる時期ですので、Idempotency-Keyによる二重実行防止と、リトライ3〜5回にJitterを付けるという数値Gateを標準装備にしておきたいところです。

中小SaaS ― マネージドに寄せて少人数で回す ― 生成AI時代のアーキテクチャ超入門senkohome.com/arch-intro-case-saas/

大企業の場合

マイクロサービスや外部APIを多用する構成では、さらにCircuit BreakerとBulkhead、Rate Limitが必要になりますが、これらはIstioのようなサービスメッシュで外側から入れるのが効率的だと思います。決済・金融・在庫系であれば、厳格な冪等性とトランザクション設計(SagaやOutbox)まで必須になってきます。

大企業基幹系 ― 新しい技術より組織で成立する設計 ― 生成AI時代のアーキテクチャ超入門senkohome.com/arch-intro-case-enterprise/

AI判断軸 ― AIのコードはエラーハンドリングが甘い

AIが生成するコードの典型的な穴

AIにコード生成を任せると、正常系は正確に書きますがエラーハンドリングが不十分になりがちです。try-catchで全例外を握り潰す、エラー時に空配列を返して後続処理が壊れる、リトライ回数を無限にする──AI生成コードでよく見る問題です。プロジェクトのエラー分類ルールとそれぞれの処理方針をドキュメント化してAIのコンテキストに含めることで、精度が上がります。「try/catchで囲んだから大丈夫」はAI生成コードの定番の罠で、握り潰しか否かをレビューで見抜くのが人間の仕事です。

Result型の導入がAIの安全なコード生成を促す

TypeScriptの Result<T, E> のような型でエラーを表現するルールがあれば、AIは「この関数は失敗する可能性がある」ことを型から理解し、呼び出し側でエラーハンドリングを必ず書きます。例外のthrowだけに頼る設計では、AIがcatchを書き忘れる事故が起きやすい。型はAIへの最も確実な指示書です。

やってはいけないこと

サイレント障害・二重処理・雪崩停止の原因になる典型を、特に危険な6つに絞ります。

禁じ手なぜダメか → どうするか
catch (e) { } の握り潰しサイレント障害の温床になる → 境界で集約し、必ずログ+変換する
全例外を同じ扱いでcatchバグと業務エラーが混ざりアラートが形骸化する → エラー型を階層化する
タイムアウト無指定相手が詰まった瞬間スレッド枯渇で全停止する → 全外部呼び出しに設定する
リトライに冪等性キーなし二重決済・二重登録が起きる → Idempotency-Keyを先に設計する
リトライにJitterなしThundering Herdで復旧中のサービスを再度落とす → バックオフ+Jitterにする
ユーザーにスタックトレースを表示攻撃の手がかりになる → ユーザー向けメッセージ+相関IDに変換する

2012年のKnight Capital事件(45分で4.4億ドル損失)は、古いコードが残った1台のサーバーのエラー処理不備が発端でした。エラー設計は「起きてから足す」では絶対に間に合いません。

筆者メモ ― 「タイムアウトなし」が生んだ雪崩

2020年11月のAWS大規模障害(us-east-1 Kinesisの停止、影響約17時間)は、「スレッド枯渇」が起点の雪崩として語り継がれています。CloudWatch・Cognito・SQSなど多数のサービスが連鎖的に影響を受け、教科書的な「外部依存の遅延連鎖」が現実規模で発生しました。

この手の事故は企業内でも日常茶飯事です。「外部APIを呼ぶHTTPクライアントにタイムアウトを設定し忘れたまま何年も動いていた」という話はあちこちで聞かれます。普段は数十ミリ秒で返ってくるから問題にならないだけで、ある日相手が詰まった瞬間、スレッドが永遠に掴まれてプロセス全体が応答不能になる。タイムアウト・Circuit Breaker・Bulkheadは「起きてから足す」では絶対に間に合わない、というのが共通の教訓です。

決めるべきこと — 自分のプロジェクトでの答えは?

以下の項目について、自分のプロジェクトの答えを1〜2文で言語化してみてください。曖昧なまま着手すると、必ず後から「なぜそう決めたんだっけ」が問われます。

  • 例外 vs Result型の使い分け方針
  • エラー型の階層設計(業務 / 技術 / バグ)
  • 相関IDの発行と伝播方式OpenTelemetry等)
  • リトライ方針(バックオフ / Jitter / 最大試行回数)
  • 冪等性の実装方式Idempotency-Keyの持ち方)
  • Circuit Breaker / Timeout / Rate Limitの閾値
  • ユーザー向けエラーメッセージのフォーマット

この記事に関連する記事

まとめ

本記事はエラーハンドリングについて、エラー分類・例外vsResult型・リトライ戦略・冪等性・Circuit Breakerまで含めて解説しました。如何だったでしょうか。

異常系の想像力は人間の仕事。AIには型と標準ライブラリで縛りをかけるのが2026年のエラー設計の現実解です。

これで「アプリケーションアーキテクチャ」カテゴリ全5記事が完結しました。次回からは「フロントエンドアーキテクチャ」カテゴリに入り、ホスティング・レンダリング・状態管理・SEOなどフロントエンドの設計判断を解説していきます。

シリーズ目次に戻る → 『生成AI時代のアーキテクチャ超入門』の歩き方

本記事で扱った内容の詳細は MDN Web Docs - Error handling も合わせて参考にしてください。

それでは次の記事も閲覧いただけると幸いです。