概要(エラーの再スローは「原因を記録して、正しい層へ渡す」ための技術)
再スロー(re-raise)は、捕まえた例外を処理(ログや補足)したうえで、呼び出し元へ再び投げ直して「適切な場所で判断」させるための手法です。重要なのは、トレースバック(どこで起きたか)を失わない再スローの仕方、例外の意味を保った「翻訳(別例外へ置き換え)」、そして「どこで止める/どこまで伝播させる」の線引きです。
基本の型(捕捉→ログ/補足→再スロー)
同じ例外をそのまま再スロー(トレースを保つ)
import logging
def load(path: str) -> str:
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
logging.error("ファイルが見つかりません: %s", path)
raise # ← 直前の例外をそのまま再スロー
Python- 例外の種類と元のトレースバックを保ったまま上位へ渡せます。
- except ブロックで補足(ログや計測)だけ行い、判断は上位層に委ねます。
別の例外へ「翻訳」して再スロー(抽象化)
class ConfigError(Exception):
pass
def read_config(path: str) -> dict:
try:
import json
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
raise ConfigError(f"設定読込に失敗: {path}") from e # 例外連鎖
Python- ドメインに沿った例外へ置き換え(翻訳)し、「なぜ失敗したか」を表現します。
- from e を付けると「元の原因(FileNotFoundError など)」と連鎖して、原因追跡が容易になります。
重要ポイントの深掘り(raise と raise from/トレースの保ち方)
raise(引数なし)と raise e の違い
- raise(引数なし): 直前に捕捉した例外を「そのまま」再スロー。元のトレースバックを保つ。
- raise e: 新たなスローとして扱われ、トレース位置が「ここ」に変わることがある。通常は推奨しない。
現場では「トレースを失わない」ために、基本は raise(引数なし)を使います。例外を翻訳する場合は raise 新例外 from e を使い、連鎖(原因のつながり)を残します。
どこで再スローするか(層の責務)
- 下位層(I/O・外部API): 原因の詳細をログに残して「そのまま再スロー」か「ドメイン例外へ翻訳」。
- 中位層(ドメイン): ドメイン例外へ統一して上位へ再スロー。外部依存の生例外はなるべく外へ出さない。
- 上位層(UI/CLI/HTTP): 例外を最終的に「ユーザー向けのエラー応答へ変換」し、ここで止める。
実例(Web / API 文脈での再スロー設計)
例1:API呼び出しのログ+再スロー(原因を保って上へ)
import logging, requests
def fetch_user(uid: str) -> dict:
try:
r = requests.get(f"https://api.example.com/users/{uid}", timeout=5)
r.raise_for_status()
return r.json()
except requests.RequestException:
logging.error("API失敗 uid=%s", uid)
raise # 上位(サービス層)へ判断を委ねる
Python例2:サービス層で「翻訳」して再スロー(ドメイン例外へ)
class UserServiceError(Exception):
pass
def get_user_profile(uid: str) -> dict:
try:
data = fetch_user(uid)
return {"id": data["id"], "name": data.get("name", "unknown")}
except KeyError as e:
raise UserServiceError("ユーザーデータ形式が不正") from e
except Exception as e:
raise UserServiceError("ユーザー情報の取得に失敗") from e
Python例3:UI層(FastAPIなど)で最終処理(HTTPへ変換)
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/users/{uid}")
def user(uid: str):
try:
return get_user_profile(uid)
except UserServiceError as e:
raise HTTPException(status_code=502, detail=str(e)) # ここで止める(ユーザー向け)
Pythonログと再スローの両立(重複・ノイズを避ける)
ログは「一箇所で要約」し、再スローは素直に
- 下位層: 原因の観測(URL・パラメータ・レスポンス要約)をログに残す。
- 中位層: 翻訳時に「何をしたかったか」を短くログ(必要なら)。
- 上位層: 最終的なユーザー向け応答で詳細ログは避ける(機密・ノイズ配慮)。
過剰な多段ログはノイズになります。「一度だけ核心を記録」→再スローの型が読みやすいです。
例外の整形と再スロー(メッセージは具体的に、短く)
メッセージ設計の型
- 何を試みたか(GET /users、read config)
- 何が失敗したか(タイムアウト、JSON不正、キー欠落)
- 識別子(uid、ファイルパス、環境名)
def read_json(path: str) -> dict:
try:
import json
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except (OSError, json.JSONDecodeError) as e:
raise RuntimeError(f"JSON読込失敗 path={path}") from e
Python典型的な落とし穴と回避(握りつぶし・誤った再スロー・finally誤用)
except で握りつぶす(pass)は厳禁
「なぜ失敗したか」が消えると原因へ辿れません。最低限ログか再スローを入れます。
raise e の多用でトレースが「ここ」に切り替わる
原因行が見えなくなることがあります。基本は raise(引数なし)、翻訳は raise 新例外 from e。
finally で再スローを上書きしない
finallyで例外を握りつぶしたり、別の例外を投げると元の原因が消えます。後片付けに限定し、例外は except 側で扱います。
try:
...
except Exception as e:
...
raise
finally:
cleanup() # 片付けのみ。ここで raise しない
Python小さな実践ガイド(いつ再スローするか、いつ止めるか)
再スローする
- 原因を記録したいが、判断は上位層の責務
- 低レイヤの詳細(requestsの例外など)を、抽象化層へ委ねたい
止める(ここで処理する)
- ユーザー向け応答へ変換する最終地点(CLIメッセージ、HTTPステータス)
- ここで代替処理・リトライ・フォールバックを行う設計にしたい場合
まとめ(「記録して渡す」か「翻訳して止める」かを明快に)
エラーの再スローは、原因を適切に記録しつつ、判断すべき層へエラーを伝えるための中核の技術です。トレースを保つなら raise(引数なし)、意味を抽象化するなら raise 新例外 from e。下位で記録→中位で翻訳→上位で最終処理、という層ごとの役割を明確にすれば、初心者でも短いコードで「原因が見え、扱いが一貫した」エラーハンドリングを設計できます。
