この記事のポイント

  • try-except構造における設計崩壊ポイントをレビューできる
  • 例外処理の粒度設計・責務分離ミスを発見する力が身につく
  • 例外漏洩、予期せぬ成功、誤捕捉パターンをコードから読み解く

そもそもtry-exceptとは

Pythonのtry-except構文は、例外発生時に処理を分岐させる仕組みです。以下のような基本形がよく使われます。

try:
    risky_operation()
except SomeException as e:
    handle_exception(e)

これにより、エラー発生箇所とその処理を分けることができます。問題は、この便利さが過剰防御や設計ミスを招きやすいことです。

なぜこれをレビューするのか

実務でよく見るのは「とりあえずtry-exceptで囲っておく」コードです。これには以下の問題が内在します。

  • エラー検出責務の混在
  • 正常系フローの隠蔽
  • 原因特定困難なcatch-all
  • 失敗時の状態不整合

レビューアーは「例外は制御フローの代替手段ではない」という原則を頭に置きつつ、try-exceptが設計意図通り機能しているかを読み取る必要があります。

レビューアー視点

  • try-exceptで何の責務をカバーしているのか
  • 想定している例外種別は明示されているか
  • except節の中身が妥当か(ログ、リトライ、巻き戻し)
  • except節の粒度が適切か
  • except節に副作用が隠れていないか

開発者視点

  • 例外種別を事前に調査して明示すること
  • 本来起きないはずの例外は潰さず上位に伝播させること
  • except節の副作用を安易に積み上げないこと
  • 例外をビジネスロジックの分岐代わりに使わないこと

良い実装例

なぜこの実装が良いのか

  • 例外種別をDatabaseErrorに限定しており、責務の範囲が明確
  • 原因情報をロギングしており、障害調査に役立つ情報を確保
  • 例外を再送出することで上位層の障害監視が機能する
  • try-exceptの粒度が最小限で、巻き取りすぎていない
  • except節に副作用が存在せず、シンプルで読みやすい
# api_request_log_saver.py

import logging
from db_connection import insert_request_log, DatabaseError

class ApiRequestLog:
    def __init__(self, request_id, endpoint, client_ip, response_code, requested_at):
        self.request_id = request_id
        self.endpoint = endpoint
        self.client_ip = client_ip
        self.response_code = response_code
        self.requested_at = requested_at

def save_request_log(log: ApiRequestLog):
    try:
        insert_request_log(log)
    except DatabaseError as e:
        logging.error(f"DB error while saving request log: {e}")
        raise
補足

再送出によって、障害監視・上位サービス層が異常検知できる設計になっています。ここでのexceptは責務範囲を超えていません。

レビュー観点

  • 想定例外の明示有無
  • catch-allの使用有無(except:, except Exception:
  • except内での副作用実装有無
  • 状態巻き戻しや補正処理の要否検討
  • 例外情報の欠落(ログ記録に例外詳細含める)

良くない実装例: ケース1

典型的なtry-except乱用パターンを提示します。

# request_logger.py

import logging
from db_connection import insert_request_log

class ApiRequestLog:
    def __init__(self, request_id, endpoint, client_ip, response_code, requested_at):
        self.request_id = request_id
        self.endpoint = endpoint
        self.client_ip = client_ip
        self.response_code = response_code
        self.requested_at = requested_at

def save_request_log(log: ApiRequestLog):
    try:
        insert_request_log(log)
    except:
@Reviewer
例外種別が不明確です。最低限 `except Exception:` 等で限定してください。
logging.warning("Failed to save request log")
@Reviewer
例外原因をログに出力していません。障害調査困難になります。例外内容を記録してください。
@Reviewer
例外を上位に再送出しておらず、異常検知ができなくなります。原則再送出してください。

問題点

  • catch-allで何でも吸収してしまっている
  • 原因情報の欠落
  • 障害伝播阻害

改善例

# request_logger_improved.py

import logging
from db_connection import insert_request_log, DatabaseError

class ApiRequestLog:
    def __init__(self, request_id, endpoint, client_ip, response_code, requested_at):
        self.request_id = request_id
        self.endpoint = endpoint
        self.client_ip = client_ip
        self.response_code = response_code
        self.requested_at = requested_at

def save_request_log(log: ApiRequestLog):
    try:
        insert_request_log(log)
    except DatabaseError as e:
        logging.error(f"DB error: {e}")
        raise
    except Exception as e:
        logging.critical(f"Unexpected error while saving log: {e}")
        raise

事前に発生可能性のあるDatabaseErrorは通常系。想定外のExceptionはクリティカル扱いし通知強度を上げています。責務分離が整理できている例です。

良くない実装例: ケース2(副作用込みパターン)

次にtry-except内で副作用を抱え込む典型例です。

# request_logger_with_fallback.py

import logging
from db_connection import insert_request_log

class ApiRequestLog:
    def __init__(self, request_id, endpoint, client_ip, response_code, requested_at):
        self.request_id = request_id
        self.endpoint = endpoint
        self.client_ip = client_ip
        self.response_code = response_code
        self.requested_at = requested_at

def save_request_log(log: ApiRequestLog):
    try:
        insert_request_log(log)
    except:
@Reviewer
例外種別が不明確です。副作用処理を伴う場合は特に例外種を限定してください。
logging.warning("Failed to save request log")
@Reviewer
ログ記録のみならず副作用が混在しています。責務分離を検討してください。
send_alert_email("DB failed, log saving skipped") retry_insert(log)
@Reviewer
exceptブロックが肥大化しています。エラーハンドリング処理の分離が望ましいです。

問題点

  • 副作用(通知・リトライ)とエラー処理が混在
  • 捕捉粒度が曖昧
  • 責務集中

改善例

# request_logger_separated.py

import logging
from db_connection import insert_request_log, DatabaseError
from notifications import send_alert_email
from retry import retry_insert

class ApiRequestLog:
    def __init__(self, request_id, endpoint, client_ip, response_code, requested_at):
        self.request_id = request_id
        self.endpoint = endpoint
        self.client_ip = client_ip
        self.response_code = response_code
        self.requested_at = requested_at

def save_request_log(log: ApiRequestLog):
    try:
        insert_request_log(log)
    except DatabaseError as e:
        handle_db_error(e, log)

def handle_db_error(e, log):
    logging.error(f"DB error while saving request log: {e}")
    send_alert_email("DB failed during request log save.")
    retry_insert(log)

副作用ロジックはhandle_db_errorへ分離し、save_request_logの責務を単純化しています。レビューアー視点でも読みやすい構造です。

観点チェックリスト

まとめ

try-exceptはあくまで「設計の表現」であって「保険」ではありません。レビューアーは例外構造=設計意図の写像であると読み解き、単なる構文ミスではなく、責務崩壊を発見することが重要です。安易な捕捉や副作用の混入を許さず、責務を整理するレビュー技術を磨いていきましょう。