Python|raise fromの正しい使い方と例外設計ミスの防ぎ方
この記事のポイント
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有無だけでなく、その使用場所・粒度・情報保持方針まで読み解き、設計者の責務分離力そのものをレビュー対象としていきましょう。