AIが書くコードには、エラーハンドリングに関して2つの極端な傾向があります。
- 握りつぶし:
except Exception: passで全エラーを無視する - 過剰防御: あらゆる箇所にtry-exceptを入れて、本来のエラーが伝播しない
どちらも本番で「静かに壊れる」原因になります。本記事では、5層のエラーハンドリング戦略を設計し、AIが適切なレベルのエラー処理を書くための仕組みを記録します。
AIが書く典型的な「握りつぶし」
パターン1:全てをキャッチ
1# AIが書きがちなコード2def fetch_data(url):3 try:4 response = requests.get(url)5 return response.json()6 except Exception:7 return None # 何が起きても None を返すこのコードの問題点は以下の通りです。
- ネットワークエラーなのか、JSONパースエラーなのか、認証エラーなのか区別できません
- 呼び出し元は
Noneを「データなし」と「エラー」の両方の意味で受け取ります - ログにも残りません
パターン2:空リストで握りつぶし
1def get_resources():2 try:3 return api_client.fetch_resources()4 except Exception:5 return [] # エラーでも空リストを返すこれが16日間のリソース監視無効化事故の直接原因でした。認証エラーが[]に化けて、「リソースなし」として正常処理されました。
5層エラーハンドリング戦略
全体構成
1Level 1: ロギング(全てのイベントを記録)2 ↓3Level 2: 例外処理(想定されるエラーのみキャッチ)4 ↓5Level 3: リトライロジック(一時的な障害に対応)6 ↓7Level 4: 通知システム(人間に知らせる)8 ↓9Level 5: グレースフルシャットダウン(安全に停止する)Level 1:ロギング
1from myModules.commonLogger import setupLogger2logger = setupLogger(__name__)3
4def process_task(task):5 logger.info(f"[TASK] Processing: {task.name} priority={task.priority}")6 # ... 処理 ...7 logger.info(f"[TASK] Complete: {task.name} result={result}")ルール: 処理の開始と終了を必ずログに記録します。ログがない区間で障害が起きると、原因の特定が困難になります。
Level 2:例外処理
1# ❌ AIが書きがちな広すぎるキャッチ2try:3 result = api_call()4except Exception:5 result = default6
7# ✅ 想定するエラーのみキャッチ8try:9 result = api_call()10except ConnectionError:11 logger.warning("API接続エラー。リトライします")12 result = retry_api_call()13except AuthenticationError:14 logger.error("認証エラー。APIキーを確認してください")15 raise # 認証エラーは上位に伝播(握りつぶさない)ルール: except Exceptionは最上位の安全ネットとしてのみ使用します。通常の処理では具体的な例外クラスをキャッチします。
Level 3:リトライロジック
1import time2
3def api_call_with_retry(func, max_retries=3, backoff_seconds=2):4 for attempt in range(max_retries):5 try:6 return func()7 except ConnectionError:8 if attempt == max_retries - 1:9 raise # 最終試行でも失敗なら例外を伝播10 wait = backoff_seconds * (2 ** attempt) # 指数バックオフ11 logger.warning(f"リトライ {attempt+1}/{max_retries}({wait}秒後)")12 time.sleep(wait)ルール: リトライ対象は一時的な障害(ネットワークエラー、レート制限)のみです。認証エラーやバリデーションエラーはリトライしません。
Level 4:通知システム
1def notify_error(message, severity="warning"):2 if severity == "critical":3 slack.send(channel="#alerts", text=f"🚨 CRITICAL: {message}")4 elif severity == "warning":5 slack.send(channel="#monitoring", text=f"⚠️ WARNING: {message}")6 # INFO レベルはSlack通知しない(ログのみ)ルール: 通知は「人間のアクションが必要な場合」のみです。全エラーを通知すると通知疲れで見なくなります。
Level 5:グレースフルシャットダウン
1import sys2
3EXIT_SUCCESS = 04EXIT_PARTIAL_FAILURE = 1 # 一部失敗(処理は継続可能)5EXIT_ERROR = 2 # 致命的エラー(処理不可能)6
7def run():8 try:9 phase_input()10 phase_process()11 phase_output()12 sys.exit(EXIT_SUCCESS)13 except CriticalError as e:14 logger.error(f"致命的エラー: {e}")15 notify_error(str(e), severity="critical")1 collapsed line
16 sys.exit(EXIT_ERROR)ルール: 終了コードで「成功/部分失敗/致命的エラー」を区別します。プロセスマネージャーは終了コードを監視し、異常終了時にジョブを再実行できます。
CLAUDE.mdへのルール化
AIが適切なエラーハンドリングを書くように、CLAUDE.mdに以下を追加しました。
1## エラーハンドリングルール2
31. except Exception: pass は禁止4 - 具体的な例外クラスをキャッチすること5 - キャッチしたエラーは必ずログに記録すること6
72. 戻り値での握りつぶし禁止8 - エラー時に None や [] を返す場合は、9 ログにWARNING以上で記録すること10 - 認証エラーは絶対に握りつぶさない(raise すること)11
123. リトライ対象の限定13 - ConnectionError, TimeoutError のみリトライ14 - AuthenticationError, ValueError はリトライしない学んだこと
1. AIは「親切心」でエラーを握りつぶす
AIは「プログラムが止まらないように」という善意でエラーをキャッチします。しかし本番システムでは「止まること」より「静かに壊れること」の方が危険です。
2. 5層全てが必要
ログだけあっても通知がなければ気づけません。リトライだけあっても認証エラーには無意味です。5層を組み合わせて初めて堅牢になります。
3. 終了コードの設計がプロセスマネージャーとの連携に必須
プロセスマネージャーは終了コードに基づいてジョブの再実行を判断します。sys.exit(0)とsys.exit(2)を区別することで、「正常終了」と「要再実行」を自動判別できます。
まとめ
5層エラーハンドリングで重要なのは以下の3点です。
- 握りつぶし禁止をCLAUDE.mdで強制:
except Exception: pass禁止、認証エラーはraise必須です - 5層の組み合わせ: ロギング → 例外処理 → リトライ → 通知 → グレースフルシャットダウンです
- 「止まる」より「静かに壊れる」が危険: エラーを隠すコードは、16日間の障害を生みます