この内容は古いバージョンです。最新バージョンを表示するには、戻るボタンを押してください。
バージョン:5
ページ更新者:guest
更新日時:2026-06-11 07:12:00

タイトル: 例外処理
SEOタイトル: PHP 例外処理 (try/catch) 完全ガイド (Throwable / 独自例外 / Laravel)

この記事の要点
  • PHP の例外処理try / catch / finally + throw
  • PHP 7+ で Error クラスが追加 → 共通インターフェース \Throwable を catch すれば両方拾える
  • PHP 8+ で複数 catch catch (E1|E2 $e)、PHP 8+ で $e 省略可
  • 独自例外は class MyException extends Exception
  • Laravel は app/Exceptions/Handler.php でグローバルにハンドリング

基本構文 try / catch / finally

try {
    $result = riskyOperation();
    process($result);
} catch (FileNotFoundException $e) {
    Log::warning("ファイルなし: " . $e->getMessage());
} catch (DatabaseException $e) {
    Log::error("DB エラー: " . $e->getMessage());
    throw $e;   // 再 throw
} catch (Exception $e) {
    Log::error("予期せぬエラー", ['exception' => $e]);
} finally {
    cleanup();  // 例外発生有無に関わらず必ず実行
}

throw 文と Exception クラス

// 基本
throw new Exception("エラーメッセージ");

// メッセージ + コード + 前の例外
throw new Exception("ユーザー保存失敗", 500, $previous);

// よく使う SPL 例外
throw new InvalidArgumentException("name は必須");
throw new RuntimeException("接続に失敗しました");
throw new LogicException("プログラムの論理エラー");
throw new OutOfBoundsException("配列の範囲外");
throw new UnexpectedValueException("予期しない値: $x");
throw new DomainException("値が定義域外");

// PHP 8+ は throw が「式」になり一行で書ける
$user = User::find($id) ?? throw new NotFoundException("user $id");

Exception の階層

Throwable (interface)              ★ PHP 7+
├── Exception                      ユーザー例外の基底
│   ├── RuntimeException           実行時エラー
│   │   ├── OutOfBoundsException
│   │   ├── UnexpectedValueException
│   │   └── PDOException           DB エラー
│   ├── LogicException             プログラムの論理エラー
│   │   ├── InvalidArgumentException
│   │   ├── DomainException
│   │   ├── LengthException
│   │   └── OutOfRangeException
│   ├── ErrorException             set_error_handler で変換
│   └── (ユーザー定義例外)
└── Error                          ★ PHP 7+ システムエラー
    ├── TypeError                  型エラー
    ├── ValueError                 ★ PHP 8+ 値エラー
    ├── ArgumentCountError         引数不足
    ├── ArithmeticError
    │   └── DivisionByZeroError
    ├── AssertionError
    └── ParseError                 構文エラー

Throwable で全部キャッチ (PHP 7+)

PHP 7 以前はパースエラーや TypeError は catch できませんでしたが、PHP 7+ では \Throwable インターフェースで Exception と Error を共通キャッチできます。

try {
    $result = mightFail();
} catch (\Throwable $t) {
    // Exception も Error も両方拾う
    Log::critical("最終手段", [
        'class' => get_class($t),
        'message' => $t->getMessage(),
        'file' => $t->getFile(),
        'line' => $t->getLine(),
        'trace' => $t->getTraceAsString(),
    ]);
    throw $t;
}

複数の例外を 1 つの catch (PHP 8+)

try {
    sendEmail();
} catch (NetworkException | TimeoutException | SmtpException $e) {
    // ネットワーク系を一括ハンドリング
    retry();
}

// PHP 8+ は $e 不要なら省略可
try {
    cleanup();
} catch (Exception) {
    // 黙って無視 (基本やめましょう)
}

独自例外クラス

namespace App\Exceptions;

class UserNotFoundException extends \RuntimeException
{
    private int $userId;

    public function __construct(int $userId, ?\Throwable $previous = null)
    {
        $this->userId = $userId;
        parent::__construct("User #$userId not found", 404, $previous);
    }

    public function getUserId(): int
    {
        return $this->userId;
    }
}

// 使用
try {
    $user = User::find($id);
    if (!$user) {
        throw new UserNotFoundException($id);
    }
} catch (UserNotFoundException $e) {
    abort(404, "User {$e->getUserId()} not found");
}

エラーから例外への変換

PHP の警告 (E_WARNING) や通知 (E_NOTICE) は通常 catch できませんが、set_error_handler で変換できます。

// アプリ起動時に登録
set_error_handler(function($severity, $message, $file, $line) {
    if (!(error_reporting() & $severity)) {
        return false;
    }
    throw new \ErrorException($message, 0, $severity, $file, $line);
});

// これで file_get_contents の警告も例外として扱える
try {
    $data = file_get_contents("does-not-exist.txt");
} catch (\ErrorException $e) {
    Log::warning("ファイル取得失敗: " . $e->getMessage());
    $data = "";
}

finally の使いどころ

$fh = fopen("data.txt", "r");
try {
    process(fread($fh, 1024));
} finally {
    fclose($fh);   // 例外発生しても必ず閉じる
}

// DB トランザクション
$db->beginTransaction();
try {
    $db->insert(...);
    $db->insert(...);
    $db->commit();
} catch (\Throwable $e) {
    $db->rollback();
    throw $e;
}

// Laravel ヘルパ (上記の簡略版)
DB::transaction(function () {
    User::create(...);
    Order::create(...);
});

Laravel での例外ハンドリング

// app/Exceptions/Handler.php (Laravel 10 以前)
namespace App\Exceptions;

class Handler extends ExceptionHandler
{
    public function register(): void
    {
        // 特定例外をログから除外
        $this->dontReport([
            UserNotFoundException::class,
        ]);

        // カスタムレンダリング
        $this->renderable(function (UserNotFoundException $e, $request) {
            if ($request->expectsJson()) {
                return response()->json(['error' => $e->getMessage()], 404);
            }
            return response()->view('errors.user-not-found', [], 404);
        });

        // 共通のロギング
        $this->reportable(function (\Throwable $e) {
            Sentry::captureException($e);
        });
    }
}

// Laravel 11+ は bootstrap/app.php で
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (UserNotFoundException $e) {
        return response()->json(['error' => 'not found'], 404);
    });
})

アンチパターン: グローバル try/catch (Pokemon catch)

// ❌ 全部捕まえて無視
try {
    doEverything();
} catch (\Throwable $e) {
    // 何もしない or echo "Error"; だけ
}
// → バグが隠れる、デバッグ不能

// ❌ catch して即 echo
try {
    saveUser($data);
} catch (\Exception $e) {
    echo $e->getMessage();   // ユーザーに内部エラー暴露 + ログに残らない
}

// ✅ ログを出す + 上位に再 throw
try {
    saveUser($data);
} catch (\Throwable $e) {
    Log::error("ユーザー保存失敗", ['exception' => $e, 'data' => $data]);
    throw $e;  // 上位 (グローバルハンドラ) で適切に処理
}

例外設計のベストプラクティス

  • 「想定済の異常系」だけ例外。普通の制御フローに使わない (性能劣化)
  • 適切な粒度の独自例外を定義し、catch する側で分岐できるように
  • 例外の元情報を失わない → 再 throw するときは throw new Wrapper($msg, 0, $previous)
  • finally でリソース解放 (DB / ファイル / ロック)
  • 本番のレスポンスに内部エラー詳細を出さない (情報漏洩)

FAQ

Q: @ で警告を抑制したい
A: @file_get_contents(...) は警告を抑制しますが、デバッグ困難 + 性能劣化。try/catch + set_error_handler 推奨。

Q: ExceptionRuntimeException どちらを継承?
A: 一般的なアプリ層は \RuntimeException 系、引数バリデーションは \InvalidArgumentException、論理矛盾は \LogicException がセオリー。

Q: 例外と戻り値、どちらでエラーを表現?
A: 異常系は例外 (例: DB 接続失敗)、業務ロジックの結果は戻り値 (例: ユーザー認証失敗) が定石。Result 型風に ['ok' => bool, 'data' => ...] を返す設計もアリ。