エラーハンドリング完全ガイド:設計・実装・運用における実践的ベストプラクティス

エラーハンドリングとは何か — 概念と目的

エラーハンドリング(エラー処理)とは、ソフトウェアが異常事態や予期しない入力、外部システムの障害などに遭遇したときにそれを検出、伝播、対処、そして必要なら復旧する一連の仕組みと方針を指します。単に「例外を投げる/捕まえる」だけでなく、ログ記録、ユーザーへの通知、リトライ、フォールバック、監視やアラートの設計まで含めた広い設計課題です。

なぜ重要か

  • 信頼性向上:適切なエラーハンドリングはシステムの予測可能性と可用性を高める。
  • セキュリティ:エラー情報の扱いを誤ると機密情報漏洩や攻撃につながる。
  • 運用性(Observability):正しいログとメトリクスは運用・障害対応を容易にする。
  • ユーザー体験:適切なエラーメッセージとフォールバックで、ユーザーの混乱を最小化できる。

エラーの分類

  • 回復不能(Fatal) vs 回復可能(Recoverable): サービス停止が避けられないか、代替パス・リトライで対応できるか。
  • 同期エラー vs 非同期エラー: 同期関数内の例外、非同期コールバックやキュー処理での失敗。
  • 短期的(Transient) vs 永続的(Permanent): たとえば一時的なネットワーク障害はリトライで回復し得る。
  • ユーザー入力エラー vs システム内部エラー: UX上の扱い(ユーザーに再入力させる等)が変わる。

言語・環境ごとの典型的手法

言語やランタイムによって推奨されるエラーハンドリングの形は異なります。

  • 例外ベース(Java、C#、JavaScript): try/catch/finally構造で例外を捕捉し、必要に応じてログや再送を行う。
  • 戻り値エラー(Go): error型を返し呼び出し側で明示的に処理する。例外的な制御フローを避ける設計。
  • Result/Option型(Rust、最近の関数型言語): コンパイル時にエラー処理を強制することで安全性を高める。

実践テクニックとパターン

  • Try/Catch/Finally: リソース解放や後片付けをFinallyで確実に行う。
  • エラーのラップ(Wrap): 下位レイヤの詳細を保持しつつ上位に意味のある文脈を付与する(例: "DBクエリ失敗: ユーザーID=1234")。
  • リトライ+指数バックオフ: 一時的エラーに対する一般的対策。ただし無限リトライは避ける。
  • サーキットブレーカー(Circuit Breaker): ある外部依存が多数失敗したら一定期間呼び出しを停止し、システム全体の崩壊を防ぐ。
  • バルクヘッド(Bulkhead): リソースを隔離することで、部分的な障害が全体に波及するのを防ぐ。
  • フォールバック(Fallback): メイン処理が失敗したときに代替処理(キャッシュ返却、デグレード機能)を行う。

ログ・監視・可観測性(Observability)

エラーが発生したときに「何が」「いつ」「どこで」「どの程度」の情報が得られるかが重要です。ログは人が調査するため、トレースは分散システムの経路解析、メトリクスはアラートの基準、イベントはインシデントのトリガーとして使います。

  • 構造化ログ(JSONなど): 検索と集計が容易になる。
  • トレース(分散トレーシング): リクエストを跨ぐ障害の因果関係を特定する(OpenTelemetry 等)。
  • エラーレートやエラー数をメトリクス化し閾値でアラートを出す。

ユーザーへの見せ方(UX)

ユーザーに対するエラー表示は、技術情報をそのまま見せるのではなく、次に何をすればよいかを示すべきです。

  • 内部情報は隠す(スタックトレースやDBエラーなどの内部情報は漏らさない)。
  • 再試行ボタンや問い合わせ先、障害発生中の代替案を示す。
  • トランジェントな問題の場合は「あとで再試行してください」などの文言でユーザー期待値を管理する。

セキュリティと情報漏洩防止

エラーメッセージやログにパスワード、APIキー、個人情報を含めないことは必須です。攻撃者はエラーメッセージから内部構造や脆弱性を見つけることがあるため、公開向けメッセージと内部ログを分離する運用が重要です。

テスト戦略

  • ユニットテストで成功ケースだけでなく失敗ケースも網羅する(異常系テスト)。
  • 統合テストで外部依存の異常(タイムアウト、HTTP 5xx等)をシミュレートする。
  • カオスエンジニアリング(障害注入)でシステムの耐性を評価する。

運用でよくある落とし穴

  • エラーを黙殺(catchして何もしない)して問題を隠す。
  • 情報量が多すぎるログでノイズが増え重要なシグナルを見逃す。
  • リトライを無制限に行い、かえって外部サービスに負荷をかける。
  • 例外をただ上位に投げ続けるだけでコンテキストが失われる。

実例コード(簡潔な比較)

言語ごとの典型例を示します(理解の補助)。

JavaScript(例外):

try {
  const res = await fetch(url);
  if (!res.ok) throw new Error('ネットワーク応答が不正');
  // ...
} catch (err) {
  console.error('API呼び出し失敗', err);
  // UI用に汎用メッセージを返す
}

Go(戻り値エラー):

res, err := http.Get(url)
if err != nil {
  // ネットワークエラー
  log.Println("取得失敗:", err)
  return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
  return fmt.Errorf("HTTP %d", res.StatusCode)
}

Rust(Result):

fn fetch() -> Result

運用上のチェックリスト(実務で使える)

  • エラー分類が設計書/運用手順に明記されているか。
  • 重要なエラーは必ず構造化ログとトレースを出しているか。
  • 公開向けメッセージと内部ログの分離はできているか。
  • 一時的エラーに対するリトライ戦略(最大回数・バックオフ・ジッター)を定義しているか。
  • サーキットブレーカーやフォールバックは必要な箇所に導入されているか。
  • 障害注入や異常系テストで問題が検出されているか。

まとめ

エラーハンドリングは単なる「例外を捕まえる」処理以上のもので、設計・実装・運用が一体となる領域です。適切に設計されたエラーハンドリングはシステムの信頼性、セキュリティ、運用効率、そしてユーザー体験を大きく改善します。言語特性に合わせつつ、ログ・トレース・メトリクスを組み合わせ、リトライやサーキットブレーカーといったパターンを取り入れることで、より堅牢なシステムを作れます。常に「何が起きるか」を想定し、失敗を前提とした設計を行うことが重要です。

参考文献