Python | Web / API:エラーの再スロー

Python Python
スポンサーリンク

概要(エラーの再スローは「原因を記録して、正しい層へ渡す」ための技術)

再スロー(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。下位で記録→中位で翻訳→上位で最終処理、という層ごとの役割を明確にすれば、初心者でも短いコードで「原因が見え、扱いが一貫した」エラーハンドリングを設計できます。

タイトルとURLをコピーしました