この記事のポイント

  • raise fromを正しく設計に活かすレビュー観点が理解できる
  • 例外連鎖の正しい活用と誤用パターンを見抜ける
  • 障害調査性と設計責務の崩壊をレビューで指摘できる

そもそもraise fromとは

Pythonでは例外を送出する際に、新しい例外へ原因例外を連鎖させるための構文としてraise fromが用意されています。

try:
    risky_operation()
except SomeException as e:
    raise AnotherException("失敗しました") from e

from e を付けることで、スタックトレース上で「原因となった例外」と「送出した例外」の因果関係を保つことができます。

  • 原因例外(cause)として保持
  • スタックトレースが2段構成で出力される
  • 障害調査時の解析精度が向上する

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

例外設計では「エラーの原因を消さない」「障害調査しやすくする」ことが非常に重要です。
しかし実務では以下のような誤用が頻出します。

  • 原因例外の握り潰し
  • スタックトレースが断絶して原因不明化
  • 無理に抽象例外へ変換する設計崩壊

レビューアーは、責務移譲と原因保持のバランスを読み取ることが求められます。

レビューアー視点

  • 原因例外を握り潰していないか
  • 例外変換は責務境界を意識して行われているか
  • 例外情報のロスが発生していないか
  • ユーザーメッセージ用のラップ設計が適切か

開発者視点

  • 原因例外は基本的にfromで連鎖保持する
  • ビジネス層とインフラ層で例外型の階層を分ける
  • ログ出力は新旧例外どちらも十分な情報を含める
  • 不要な例外再送出を乱発しない

良い実装例

なぜこの実装が良いのか

  • DBアクセス層とサービス層の責務境界で例外を変換
  • 原因例外はfromで完全保持しスタックトレースを維持
  • ログでも新旧例外情報を出力
  • 設計者の意図が明確に例外階層へ反映されている
# api_request_service.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

class SaveLogError(Exception):
    """サービス層用の例外"""
    pass

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 SaveLogError("リクエストログ保存失敗") from e
補足

サービス層ではSaveLogErrorのみ扱い、インフラ層の例外型は外部非公開にして分離されています。原因はfromにより保持されており障害調査性も確保されています。

レビュー観点

  • from を適切に活用しているか
  • 原因例外情報の保持有無
  • 責務層ごとに例外型が整理されているか
  • ログに原因例外の情報を含めているか
  • 無意味な例外再送出や多重ラップが発生していないか

良くない実装例: ケース1(原因例外の握り潰し)

# request_logger_flatten.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

class SaveLogError(Exception):
    pass

def save_request_log(log: ApiRequestLog):
    try:
        insert_request_log(log)
    except DatabaseError:
@Reviewer
原因例外が消失しています。fromで連鎖保持してください。
raise SaveLogError("保存失敗")
@Reviewer
障害調査時に根本原因が分からなくなります。例外情報の保持が必要です。

問題点

  • 原因となったDatabaseErrorが消失
  • スタックトレース上で何が起きたか不明瞭
  • 調査負荷が大幅に上昇

改善例

# request_logger_fixed.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

class SaveLogError(Exception):
    pass

def save_request_log(log: ApiRequestLog):
    try:
        insert_request_log(log)
    except DatabaseError as e:
        raise SaveLogError("保存失敗") from e

例外原因の連鎖を正しく保持するだけで、障害解析の工数が劇的に改善されます。raise fromは極力常用すべき基本装備です。

良くない実装例: ケース2(多重ラップ設計ミス)

# request_logger_overwrap.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

class SaveLogError(Exception):
    pass

class ApplicationError(Exception):
    pass

def save_request_log(log: ApiRequestLog):
    try:
        insert_request_log(log)
    except DatabaseError as e:
        raise SaveLogError("保存失敗") from e
    except Exception as e:
@Reviewer
不要な汎用ラップで例外が過剰にネストしています。ラップは最小限に整理してください。
raise ApplicationError("アプリケーション例外") from e

問題点

  • 無意味な多重変換で原因階層が肥大化
  • 調査ログが冗長化して可読性低下
  • 設計上の例外階層責務が曖昧

改善例

改善例
# request_logger_simplified.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

class SaveLogError(Exception):
    pass

def save_request_log(log: ApiRequestLog):
    try:
        insert_request_log(log)
    except DatabaseError as e:
        raise SaveLogError("保存失敗") from e
    except Exception:
@Reviewer
👍想定外例外は上位へそのまま再送出するほうが適切です。握り潰さず伝播させましょう。
raise

例外階層は責務境界ごとに最小限化し、fromによる因果情報だけを維持しています。ネスト過剰はレビューで早期是正すべき設計臭です。

観点チェックリスト

まとめ

raise fromは単なる構文の便利機能ではありません。
設計意図を正しく写像し、障害調査性を保ち、責務境界を明示する例外設計の要石です。レビューアーはfrom有無だけでなく、その使用場所・粒度・情報保持方針まで読み解き、設計者の責務分離力そのものをレビュー対象としていきましょう。