PHPの例外処理について

採用はこちら

PHPの例外処理とは、プログラムの実行中に発生したエラーや想定外の状態を、通常の処理とは別の流れで安全に扱うための仕組みです。

たとえば、Webアプリケーションでは次のような場面でエラーが発生することがあります。

  • データベース接続に失敗した
  • 外部APIとの通信に失敗した
  • 指定されたファイルが存在しない
  • 関数に不正な値が渡された
  • ログインしていないユーザーが会員ページにアクセスした
  • 決済処理やメール送信に失敗した
  • 想定していたデータが取得できなかった

このような問題が起きたとき、何も対策していないと、プログラムが途中で停止したり、ユーザーに内部エラーがそのまま表示されたりする可能性があります。

そこで使うのが、PHPの例外処理です。

例外処理を使うと、エラーが発生した場合に、ログを残したり、ユーザーに分かりやすいメッセージを表示したり、データベースの変更を取り消したりできます。

目次

PHPの例外処理で使う基本構文

PHPの例外処理では、主に次の4つを使います。

キーワード役割
try例外が発生する可能性のある処理を書く
throw例外を発生させる
catch発生した例外を受け取って処理する
finally例外の有無に関係なく最後に実行する処理を書く

基本的な形は次のとおりです。

try {
    // 例外が発生する可能性のある処理
} catch (Exception $e) {
    // 例外が発生したときの処理
}

finally を含める場合は、次のように書きます。

try {
    // 通常処理
} catch (Exception $e) {
    // 例外発生時の処理
} finally {
    // 成功・失敗に関係なく実行する処理
}

try ブロックには、例外が発生する可能性のある処理を書きます。

その中で例外が発生すると、通常の処理はいったん中断され、対応する catch ブロックへ処理が移ります。

try-catchの基本的な使い方

最もシンプルな例

まずは、基本的な例を見てみましょう。

<?php

try {
    throw new Exception('処理に失敗しました');
} catch (Exception $e) {
    echo 'エラー: ' . $e->getMessage();
}

このコードでは、throw new Exception() によって例外を発生させています。

throw new Exception('処理に失敗しました');

発生した例外は、次の catch で受け取られます。

catch (Exception $e)

そして、$e->getMessage() によって例外メッセージを取得しています。

実行結果のイメージは次のとおりです。

エラー: 処理に失敗しました

throwで例外を発生させる

throwの基本

throw は、例外を発生させるために使います。

throw new Exception('エラーが発生しました');

実務では、関数の中で条件に応じて例外を投げることが多いです。

<?php

function divide(int $a, int $b): float
{
    if ($b === 0) {
        throw new Exception('0で割ることはできません');
    }

    return $a / $b;
}

try {
    echo divide(10, 0);
} catch (Exception $e) {
    echo 'エラー: ' . $e->getMessage();
}

この例では、divide(10, 0) を実行したときに、第二引数が 0 なので例外が発生します。

通常であれば割り算の結果を返しますが、例外が発生した場合は return まで進まず、catch に処理が移ります。

PHP 8.0以降ではthrowを式として使える

PHP 8.0以降では、throw を式として使うこともできます。

たとえば、次のような書き方が可能です。

$user = $session->user ?? throw new Exception('ユーザー情報がありません');

このコードでは、$session->user が存在すればその値を $user に代入します。

一方、$session->usernull の場合は、例外を発生させます。

初心者向けのコードでは無理に使う必要はありませんが、現代的なPHPでは見かけることがある書き方です。

catchで例外を受け取る

catchの基本

catch は、発生した例外を受け取って処理するために使います。

try {
    throw new Exception('ユーザー情報の取得に失敗しました');
} catch (Exception $e) {
    echo $e->getMessage();
}

catch (Exception $e)$e には、例外に関する情報が入っています。

この $e は例外オブジェクトです。

例外オブジェクトで使える主なメソッド

例外オブジェクトでは、次のようなメソッドをよく使います。

メソッド内容
$e->getMessage()例外メッセージを取得する
$e->getCode()例外コードを取得する
$e->getFile()例外が発生したファイルを取得する
$e->getLine()例外が発生した行番号を取得する
$e->getTrace()スタックトレースを配列で取得する
$e->getTraceAsString()スタックトレースを文字列で取得する
$e->getPrevious()前の例外を取得する

たとえば、次のように使えます。

<?php

try {
    throw new Exception('ユーザー情報の取得に失敗しました', 500);
} catch (Exception $e) {
    echo 'メッセージ: ' . $e->getMessage() . PHP_EOL;
    echo 'コード: ' . $e->getCode() . PHP_EOL;
    echo 'ファイル: ' . $e->getFile() . PHP_EOL;
    echo '行番号: ' . $e->getLine() . PHP_EOL;
}

ただし、本番環境では、ファイル名や行番号、スタックトレースを画面に表示するのは避けるべきです。

内部構造がユーザーに見えてしまうと、セキュリティ上のリスクになる可能性があります。

そのため、実務では詳細情報をログに残し、ユーザーには分かりやすいメッセージだけを表示するのが基本です。

catch (Exception $e) {
    error_log($e->getMessage());

    echo 'エラーが発生しました。時間をおいて再度お試しください。';
}

finallyの使い方

finallyとは

finally は、例外が発生してもしなくても最後に実行される処理を書くためのブロックです。

<?php

try {
    echo "処理開始\n";

    throw new Exception('エラーが発生しました');

} catch (Exception $e) {
    echo "catch: " . $e->getMessage() . "\n";

} finally {
    echo "finally: 後処理を実行します\n";
}

実行結果のイメージは次のとおりです。

処理開始
catch: エラーが発生しました
finally: 後処理を実行します

finally は、次のような後処理でよく使います。

  • ファイルを閉じる
  • 一時ファイルを削除する
  • ロックを解除する
  • 接続を閉じる
  • 共通の後処理を実行する

ファイル操作でfinallyを使う例

<?php

$file = null;

try {
    $file = fopen('data.txt', 'r');

    if (!$file) {
        throw new RuntimeException('ファイルを開けませんでした');
    }

    // ファイルを使った処理

} catch (RuntimeException $e) {
    error_log($e->getMessage());

    echo 'ファイル処理に失敗しました';

} finally {
    if (is_resource($file)) {
        fclose($file);
    }
}

このコードでは、ファイルを開く処理に成功しても失敗しても、最後に finally が実行されます。

ファイルが開かれている場合は、fclose() で閉じます。

finallyを使うときの注意点

finally は通常、例外が発生してもしなくても実行されます。

また、trycatch の中で return した場合でも、finally は実行されます。

<?php

function test(): string
{
    try {
        return 'tryのreturn';
    } finally {
        echo 'finallyが実行されます';
    }
}

echo test();

実行結果のイメージは次のとおりです。

finallyが実行されますtryのreturn

ただし、finally の中で return する書き方は避けた方が安全です。

<?php

function test(): string
{
    try {
        return 'try';
    } finally {
        return 'finally';
    }
}

echo test();

この場合、tryreturn よりも、finallyreturn が優先されます。

実行結果は次のようになります。

finally

処理の流れが分かりにくくなるため、実務では finally の中で return する書き方はあまりおすすめできません。

また、exitdie、プロセスの強制終了、重大なメモリ不足など、スクリプト自体が終了するケースでは、通常の後処理として finally が期待通りに動かない可能性もあります。

ExceptionとThrowableの違い

Throwableとは

PHPの例外処理を理解するうえで、ExceptionThrowable の違いは重要です。

PHP 7以降では、投げられるエラーや例外の大本に Throwable というインターフェースがあります。

構造を簡単に表すと、次のようになります。

Throwable
├── Exception
└── Error

Throwable は、PHPで投げられるもの全体を扱うための型です。

Exception だけでなく、TypeErrorError なども含めて捕捉したい場合は、Throwable を使います。

Exceptionとは

Exception は、アプリケーション側で発生させる例外の基本クラスです。

throw new Exception('通常の例外です');

たとえば、次のようなケースで使われます。

  • ユーザーが見つからない
  • 商品在庫が足りない
  • 外部APIへの接続に失敗した
  • ファイルの読み込みに失敗した
  • 不正な引数が渡された
  • 権限がない

つまり、アプリケーションの処理上で想定される異常を表すときに使うことが多いです。

Errorとは

Error は、PHPの実行時エラーを表すクラスです。

代表的なものには、次のようなクラスがあります。

クラス内容
ErrorPHP内部エラーの基本クラス
TypeError型が合わない場合に発生する
ValueError値が不正な場合に発生する
ParseError構文解析エラー
DivisionByZeroError0除算が発生した場合
ArgumentCountError引数の数が合わない場合

たとえば、型が合わない値を渡すと TypeError が発生することがあります。

<?php

function greet(string $name): void
{
    echo "こんにちは、{$name}さん";
}

try {
    greet([]);
} catch (TypeError $e) {
    echo '型エラー: ' . $e->getMessage();
}

この例では、greet() は文字列を受け取る関数ですが、配列を渡しているため TypeError が発生します。

Exceptionだけをcatchする場合

Exception だけを捕捉する場合は、次のように書きます。

try {
    throw new Exception('例外です');
} catch (Exception $e) {
    echo $e->getMessage();
}

この場合、Exception 系の例外は捕捉できます。

しかし、TypeError などの Error 系は捕捉できません。

Throwableをcatchする場合

ExceptionError の両方を捕捉したい場合は、Throwable を使います。

<?php

try {
    greet([]);
} catch (Throwable $e) {
    echo 'エラー: ' . $e->getMessage();
}

catch (Throwable $e) と書くと、Exception だけでなく、TypeError などの Error 系も捕捉できます。

ただし、何でも Throwable で捕捉すればよいわけではありません。

catch (Throwable $e) {
    // 何もしない
}

このように、すべてのエラーを握りつぶしてしまうと、重大なバグに気づけなくなります。

Throwable を使う場合は、必ずログを残す、ユーザーには適切なメッセージを返す、必要に応じて再スローするなどの設計が重要です。

複数のcatchを書く方法

例外の種類ごとに処理を分ける

例外処理では、複数の catch を書くことができます。

<?php

try {
    throw new InvalidArgumentException('不正な引数です');

} catch (InvalidArgumentException $e) {
    echo '入力値エラー: ' . $e->getMessage();

} catch (RuntimeException $e) {
    echo '実行時エラー: ' . $e->getMessage();

} catch (Exception $e) {
    echo 'その他の例外: ' . $e->getMessage();
}

このように書くと、例外の種類に応じて処理を分けられます。

入力値エラーならユーザーに修正を促す、システムエラーならログを残して一般的なメッセージを表示する、といった対応がしやすくなります。

catchは具体的な例外から書く

複数の catch を書く場合は、具体的な例外を先に書き、広い例外を後に書きます。

悪い例です。

try {
    throw new InvalidArgumentException('不正な引数です');

} catch (Exception $e) {
    echo '例外です';

} catch (InvalidArgumentException $e) {
    echo '不正な引数です';
}

InvalidArgumentExceptionException の一種です。

そのため、先に catch (Exception $e) があると、InvalidArgumentException はそこで捕捉されてしまいます。

後ろの catch (InvalidArgumentException $e) には到達しません。

よい例です。

try {
    throw new InvalidArgumentException('不正な引数です');

} catch (InvalidArgumentException $e) {
    echo '不正な引数です';

} catch (Exception $e) {
    echo 'その他の例外です';
}

catchの順番は、次のように考えると分かりやすいです。

catch (UserNotFoundException $e) {
    // 具体的な例外
} catch (RuntimeException $e) {
    // やや広い例外
} catch (Exception $e) {
    // さらに広い例外
} catch (Throwable $e) {
    // 最も広い捕捉
}

複数の例外型をまとめてcatchする

同じ処理でよい例外をまとめる

PHPでは、複数の例外型をまとめて捕捉できます。

<?php

try {
    // 何らかの処理
} catch (InvalidArgumentException | RuntimeException $e) {
    echo '処理可能なエラー: ' . $e->getMessage();
}

このように、| を使うことで、複数の例外型を1つの catch で扱えます。

たとえば、ユーザーに同じメッセージを表示しつつ、ログには詳細を残したい場合に便利です。

try {
    // API通信やDB処理
} catch (InvalidArgumentException | RuntimeException $e) {
    error_log($e->getMessage());

    echo '処理を完了できませんでした。';
}

同じ対応でよい例外をまとめると、コードが読みやすくなります。

ただし、例外ごとに対応が異なる場合は、無理にまとめず個別の catch を書いた方がよいです。

独自例外クラスを作る

独自例外クラスとは

実務では、Exception をそのまま使うだけでなく、独自の例外クラスを作ることがあります。

<?php

class UserNotFoundException extends Exception
{
}

このように、Exception を継承して独自の例外クラスを作れます。

独自例外を作るメリットは、何のエラーなのかを型で判断できることです。

たとえば、ユーザーが見つからなかった場合だけ特別な処理をしたいなら、次のように書けます。

catch (UserNotFoundException $e)

例外の種類をクラス名で表せるため、コードの意図が分かりやすくなります。

独自例外クラスの例

<?php

class UserNotFoundException extends Exception
{
}

function findUser(int $id): array
{
    $users = [
        1 => ['name' => '田中'],
        2 => ['name' => '佐藤'],
    ];

    if (!isset($users[$id])) {
        throw new UserNotFoundException('ユーザーが見つかりません');
    }

    return $users[$id];
}

try {
    $user = findUser(3);

    echo $user['name'];

} catch (UserNotFoundException $e) {
    echo 'ユーザー検索エラー: ' . $e->getMessage();

} catch (Exception $e) {
    echo '予期しないエラーが発生しました';
}

この例では、指定されたIDのユーザーが存在しない場合に UserNotFoundException を投げています。

そして、catch (UserNotFoundException $e) でその例外だけを個別に処理しています。

実務で使う独自例外の例

会員登録処理では、次のような独自例外を作ると分かりやすくなります。

<?php

class ValidationException extends Exception {}
class DuplicateEmailException extends Exception {}
class MailSendException extends Exception {}

使い方の例です。

<?php

function registerUser(string $email, string $password): void
{
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        throw new ValidationException('メールアドレスの形式が正しくありません');
    }

    if (strlen($password) < 8) {
        throw new ValidationException('パスワードは8文字以上で入力してください');
    }

    if ($email === 'test@example.com') {
        throw new DuplicateEmailException('このメールアドレスは既に登録されています');
    }

    // 登録処理
    // 確認メール送信処理など
}

try {
    registerUser('test@example.com', 'password123');

    echo '登録が完了しました';

} catch (ValidationException $e) {
    echo $e->getMessage();

} catch (DuplicateEmailException $e) {
    echo $e->getMessage();

} catch (MailSendException $e) {
    error_log($e->getMessage());

    echo '登録は完了しましたが、確認メールの送信に失敗しました';

} catch (Exception $e) {
    error_log($e->getMessage());

    echo '予期しないエラーが発生しました';
}

このように例外クラスを分けると、エラーの内容ごとに適切な処理を書きやすくなります。

PHPでよく使う標準例外クラス

主な例外クラス

PHPには、標準でいくつかの例外クラスが用意されています。

例外クラス用途
Exception基本となる例外クラス
RuntimeException実行時に発生する例外
InvalidArgumentException不正な引数が渡された場合の例外
LogicExceptionプログラムのロジック上の問題
OutOfRangeException範囲外の値が指定された場合の例外
UnexpectedValueException想定外の値が返された場合の例外
PDOExceptionPDOでDB関連のエラーが発生した場合の例外

すべてを最初から覚える必要はありません。

まずは、次の3つを押さえておくとよいです。

Exception
RuntimeException
InvalidArgumentException

RuntimeException

RuntimeException は、実行時に発生する問題を表すときによく使います。

if (!file_exists($path)) {
    throw new RuntimeException('ファイルが存在しません');
}

ファイルが存在しない、外部APIが応答しない、処理の実行中に問題が起きた、といったケースで使いやすい例外です。

InvalidArgumentException

InvalidArgumentException は、不正な引数が渡された場合に使います。

function getUser(int $id): array
{
    if ($id <= 0) {
        throw new InvalidArgumentException('IDは1以上で指定してください');
    }

    // ユーザー取得処理
}

関数やメソッドの呼び出し側が不正な値を渡したことを示したい場合に便利です。

例外を再スローする

再スローとは

一度 catch した例外を、もう一度外側に投げ直すことを再スローといいます。

<?php

function process(): void
{
    try {
        throw new RuntimeException('内部処理に失敗しました');

    } catch (RuntimeException $e) {
        error_log('processでエラー: ' . $e->getMessage());

        throw $e;
    }
}

try {
    process();

} catch (RuntimeException $e) {
    echo '処理に失敗しました';
}

この例では、process() の中で発生した例外をいったん catch しています。

そこでログを残したうえで、throw $e; によって外側に再スローしています。

再スローを使う場面

再スローは、次のような場面で使います。

  • いったんログだけ残したい
  • 下位の関数ではエラーを処理しきれない
  • 上位の処理に判断を任せたい
  • 例外を別の種類に変換したい
  • トランザクションのロールバック後に、さらに上位でレスポンスを作りたい

下位レイヤーで無理に画面表示まで行うと、コードの再利用性が下がります。

そのため、下位では例外を投げ、上位でまとめて処理する設計がよく使われます。

例外をラップする

例外のラップとは

例外のラップとは、発生した例外を別の例外で包んで投げ直すことです。

<?php

class PaymentException extends Exception {}

function charge(): void
{
    try {
        // 決済API呼び出し
        throw new RuntimeException('API timeout');

    } catch (RuntimeException $e) {
        throw new PaymentException('決済処理に失敗しました', 0, $e);
    }
}

try {
    charge();

} catch (PaymentException $e) {
    echo $e->getMessage();

    $previous = $e->getPrevious();

    if ($previous) {
        error_log('元の例外: ' . $previous->getMessage());
    }
}

この例では、内部では RuntimeException が発生しています。

しかし、呼び出し元には「決済処理に失敗した」という意味を持つ PaymentException として投げ直しています。

getPreviousで元の例外を確認する

Exception のコンストラクタには、第3引数として前の例外を渡せます。

throw new PaymentException('決済処理に失敗しました', 0, $e);

そして、getPrevious() を使うと元の例外を取得できます。

$previous = $e->getPrevious();

これにより、ユーザーには分かりやすいメッセージを表示しつつ、ログには元の原因を残せます。

たとえば、ユーザーには次のように表示します。

決済処理に失敗しました

一方、ログには次のような詳細を残せます。

元の例外: API timeout

このように、画面表示用のメッセージと開発者向けの詳細情報を分けられるのが、例外ラップのメリットです。

データベース処理での例外処理

PDOと例外処理

PHPでデータベースを扱う場合、PDOを使うことが多いです。

PDOでは、データベース関連のエラーが発生したときに PDOException が投げられます。

try {
    $pdo = new PDO($dsn, $user, $password);
} catch (PDOException $e) {
    echo 'データベース接続に失敗しました';
}

ただし、本番環境では、$e->getMessage() をそのまま画面に表示しないようにしましょう。

悪い例です。

catch (PDOException $e) {
    echo $e->getMessage();
}

データベース名、SQL、ファイルパスなどが表示される可能性があります。

よい例です。

catch (PDOException $e) {
    error_log($e->getMessage());

    echo 'データベース処理に失敗しました';
}

PDOのエラーモードについて

PDOで例外を扱う場合、以前は次のようにエラーモードを明示することが一般的でした。

$pdo = new PDO(
    $dsn,
    $user,
    $password,
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    ]
);

PHP 8.0以降では、PDOのエラーモードはデフォルトで PDO::ERRMODE_EXCEPTION です。

そのため、PHP 8系だけを対象にする場合、必ずしも明示的な設定が必要とは限りません。

ただし、PHP 7系以前との互換性や、コードの意図を明確にする目的で、現在でも明示的に書くことはあります。

[
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]

このように書いておくと、「PDOのエラーは例外として扱う」という意図が読み取りやすくなります。

トランザクションと例外処理

トランザクションで例外処理が重要な理由

データベース処理では、例外処理とトランザクションを組み合わせることがよくあります。

たとえば、注文処理では次のような複数の処理を行います。

  1. 注文データを登録する
  2. 在庫数を減らす
  3. 決済情報を登録する
  4. ユーザーにポイントを付与する

このうち、途中の処理だけ失敗すると、データの整合性が崩れます。

たとえば、注文データだけ登録されているのに、在庫が減っていない状態になると問題です。

そのため、途中で失敗した場合は、すべての変更を取り消す必要があります。

このときに使うのがトランザクションです。

トランザクションの基本例

<?php

$pdo = new PDO(
    'mysql:host=localhost;dbname=test;charset=utf8mb4',
    'user',
    'password',
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    ]
);

try {
    $pdo->beginTransaction();

    $stmt = $pdo->prepare('INSERT INTO orders (user_id, total) VALUES (:user_id, :total)');
    $stmt->execute([
        ':user_id' => 1,
        ':total' => 5000,
    ]);

    $stmt = $pdo->prepare('UPDATE users SET points = points + :points WHERE id = :id');
    $stmt->execute([
        ':points' => 50,
        ':id' => 1,
    ]);

    $pdo->commit();

    echo '注文が完了しました';

} catch (Throwable $e) {
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }

    error_log($e->getMessage());

    echo '注文処理に失敗しました';
}

ここでは、beginTransaction() でトランザクションを開始しています。

すべての処理が成功した場合は、commit() で確定します。

途中で例外が発生した場合は、catch に処理が移り、rollBack() で変更を取り消します。

トランザクションではThrowableをcatchすることもある

データベース処理だけなら、PDOException を捕捉すればよいように見えます。

catch (PDOException $e)

しかし、実務ではトランザクション中に発生するエラーが、必ずしも PDOException とは限りません。

たとえば、次のような例外が発生する可能性があります。

  • RuntimeException
  • InvalidArgumentException
  • PaymentException
  • TypeError
  • 独自例外

そのため、トランザクション中の処理を安全にロールバックしたい場合は、Throwable を捕捉することがあります。

catch (Throwable $e) {
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }

    throw $e;
}

ただし、Throwable を捕捉したあとに何もせず握りつぶすのは危険です。

ロールバックやログ記録を行ったうえで、必要に応じて再スローする設計が安全です。

ファイル操作での例外処理

ファイル操作では自分で例外を投げることも多い

PHPの関数の中には、失敗しても例外を投げず、false を返すものがあります。

たとえば、file_get_contents() は失敗すると false を返すことがあります。

そのため、戻り値を確認し、自分で例外を投げる設計にすると扱いやすくなります。

<?php

function readFileContent(string $path): string
{
    if (!file_exists($path)) {
        throw new RuntimeException('ファイルが存在しません: ' . $path);
    }

    $content = file_get_contents($path);

    if ($content === false) {
        throw new RuntimeException('ファイルの読み込みに失敗しました: ' . $path);
    }

    return $content;
}

try {
    $content = readFileContent('data.txt');

    echo $content;

} catch (RuntimeException $e) {
    error_log($e->getMessage());

    echo 'ファイルを読み込めませんでした';
}

このように、失敗時に例外を投げるようにしておくと、呼び出し側でエラー処理を統一しやすくなります。

バリデーションと例外処理

バリデーションで例外を使う例

入力値のチェックでも、例外を使うことがあります。

<?php

function validateEmail(string $email): void
{
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        throw new InvalidArgumentException('メールアドレスの形式が正しくありません');
    }
}

try {
    validateEmail('invalid-email');

    echo '有効なメールアドレスです';

} catch (InvalidArgumentException $e) {
    echo $e->getMessage();
}

この例では、メールアドレスの形式が正しくない場合に InvalidArgumentException を投げています。

フォーム入力ミスは通常の分岐で扱うことも多い

ただし、フォームの入力ミスのように、ユーザーが普通に起こし得るエラーは、必ずしも例外で扱う必要はありません。

たとえば、次のようにエラー配列で返す設計もあります。

function validate(array $input): array
{
    $errors = [];

    if (empty($input['email'])) {
        $errors['email'] = 'メールアドレスを入力してください';
    }

    if (empty($input['password'])) {
        $errors['password'] = 'パスワードを入力してください';
    }

    return $errors;
}

フォーム入力エラーは、通常の分岐やエラー配列で扱う設計も多いです。

一方、フレームワークによっては、バリデーションエラーを例外として扱い、最終的にHTTPレスポンスへ変換する設計もあります。

重要なのは、プロジェクト全体で方針を統一することです。

例外に向いているエラーと向いていないエラー

例外に向いているケース

例外に向いているのは、通常の処理フローでは続行しづらいエラーです。

たとえば、次のようなケースです。

  • データベース接続に失敗した
  • 外部API通信に失敗した
  • 必須データが存在しない
  • 権限がない
  • ファイルの読み込みに失敗した
  • 決済処理に失敗した
  • メール送信に失敗した
  • 想定外の状態になった
  • トランザクション中に処理が失敗した

このようなエラーは、通常の処理を続けるのが難しいため、例外として扱うと整理しやすくなります。

例外にしない方がよいケース

一方、次のようなケースは、通常の分岐で扱うことも多いです。

  • 検索結果が0件だった
  • フォーム入力に不備があった
  • チェックボックスが未選択だった
  • ページネーションで次のページがなかった
  • ログイン画面でパスワードが間違っていた
  • 一覧表示で表示対象のデータがなかった

これらは、ユーザー操作の中で普通に起こり得る状態です。

そのため、必ずしも例外にする必要はありません。

ただし、システムやフレームワークの設計によっては例外として扱う場合もあります。

大切なのは、「何を例外とするか」をチームやプロジェクトで統一することです。

グローバル例外ハンドラ

set_exception_handlerとは

set_exception_handler() を使うと、どこにも捕捉されなかった例外を最後に処理できます。

<?php

set_exception_handler(function (Throwable $e) {
    error_log($e->getMessage());

    http_response_code(500);

    echo 'システムエラーが発生しました。';
});

throw new RuntimeException('未捕捉の例外です');

このようにしておくと、未捕捉の例外が発生した場合でも、最後にログを残したり、共通のエラーメッセージを表示したりできます。

Webアプリケーションでの使い方

Webアプリケーションでは、グローバル例外ハンドラで次のような処理を行うことがあります。

  • エラーログを記録する
  • 500エラーページを表示する
  • JSON APIならエラーレスポンスを返す
  • 管理者に通知する
  • ユーザーには詳細を見せない

ただし、すべての例外をグローバルハンドラだけで処理するのではなく、業務上意味のある例外は個別に catch することも重要です。

たとえば、「ユーザーが見つからない」は404として返し、「データベース接続エラー」は500として返す、といった使い分けです。

PHPエラーを例外に変換する

set_error_handlerとErrorException

PHPの警告やNoticeを例外として扱いたい場合、set_error_handler()ErrorException を使うことがあります。

<?php

set_error_handler(function (
    int $severity,
    string $message,
    string $file,
    int $line
): bool {
    throw new ErrorException($message, 0, $severity, $file, $line);
});

try {
    echo $undefinedVariable;

} catch (ErrorException $e) {
    echo 'エラーを例外として捕捉しました: ' . $e->getMessage();
}

このコードでは、通常のPHPエラーを ErrorException として投げ直しています。

エラーを例外化するときの注意点

すべてのPHPエラーを例外に変換すると、既存コードの挙動が大きく変わることがあります。

たとえば、これまで警告だけで処理が続いていたコードが、例外によって途中で止まる可能性があります。

そのため、エラーの例外化は慎重に導入する必要があります。

特に古いコードに対して一括で導入する場合は、影響範囲を確認しながら進めるべきです。

Webアプリケーションでの例外処理の実務例

ユーザー情報を取得する例

次の例では、ユーザー情報を取得する処理を例外で整理しています。

<?php

class UserNotFoundException extends Exception {}

function getUser(PDO $pdo, int $id): array
{
    if ($id <= 0) {
        throw new InvalidArgumentException('IDが不正です');
    }

    $stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
    $stmt->execute([':id' => $id]);

    $user = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$user) {
        throw new UserNotFoundException('ユーザーが見つかりません');
    }

    return $user;
}

try {
    $user = getUser($pdo, 10);

    echo htmlspecialchars($user['name'], ENT_QUOTES, 'UTF-8');

} catch (InvalidArgumentException $e) {
    http_response_code(400);

    echo '不正なリクエストです';

} catch (UserNotFoundException $e) {
    http_response_code(404);

    echo '指定されたユーザーは存在しません';

} catch (PDOException $e) {
    error_log($e->getMessage());

    http_response_code(500);

    echo 'データベースエラーが発生しました';

} catch (Throwable $e) {
    error_log($e->getMessage());

    http_response_code(500);

    echo '予期しないエラーが発生しました';
}

この例では、例外の種類ごとにHTTPステータスコードと表示メッセージを分けています。

例外対応
InvalidArgumentException400 Bad Request
UserNotFoundException404 Not Found
PDOException500 Internal Server Error
Throwable予期しないエラーとして500

このように整理すると、エラー内容に応じた適切なレスポンスを返しやすくなります。

APIでの例外処理

JSON APIの基本例

JSON APIでは、例外発生時にJSON形式でレスポンスを返すことが多いです。

<?php

header('Content-Type: application/json; charset=utf-8');

try {
    $data = [
        'id' => 1,
        'name' => '田中',
    ];

    echo json_encode([
        'success' => true,
        'data' => $data,
    ], JSON_UNESCAPED_UNICODE);

} catch (InvalidArgumentException $e) {
    http_response_code(400);

    echo json_encode([
        'success' => false,
        'message' => $e->getMessage(),
    ], JSON_UNESCAPED_UNICODE);

} catch (Throwable $e) {
    error_log($e->getMessage());

    http_response_code(500);

    echo json_encode([
        'success' => false,
        'message' => 'サーバーエラーが発生しました',
    ], JSON_UNESCAPED_UNICODE);
}

APIでは、フロントエンドや外部システムが扱いやすいように、レスポンス形式を統一することが重要です。

APIで内部エラーをそのまま返さない

APIで注意すべきなのは、内部エラーの詳細をそのまま返さないことです。

悪い例です。

catch (Throwable $e) {
    echo json_encode([
        'success' => false,
        'message' => $e->getMessage(),
    ]);
}

$e->getMessage() には、SQL、ファイルパス、内部処理名などが含まれる場合があります。

そのため、本番環境では次のように、ログとレスポンスを分けるのが基本です。

catch (Throwable $e) {
    error_log($e->getMessage());

    http_response_code(500);

    echo json_encode([
        'success' => false,
        'message' => 'サーバーエラーが発生しました',
    ], JSON_UNESCAPED_UNICODE);
}

ユーザーや外部システムには分かりやすいメッセージだけを返し、詳細はログで確認できるようにします。

例外処理でログを残す

ログに残すべき情報

実務では、例外を catch したら、必要に応じてログを残します。

catch (Throwable $e) {
    error_log(sprintf(
        "[%s] %s in %s:%d\n%s",
        get_class($e),
        $e->getMessage(),
        $e->getFile(),
        $e->getLine(),
        $e->getTraceAsString()
    ));

    echo 'エラーが発生しました';
}

ログに残すとよい情報は次のとおりです。

情報理由
例外クラス名どの種類のエラーか分かる
メッセージ何が起きたか分かる
ファイル名発生箇所を特定しやすい
行番号修正箇所を探しやすい
スタックトレース呼び出し経路が分かる
ユーザーID誰の操作で起きたか分かる
リクエストID複数ログを追跡しやすい
URLやHTTPメソッドどのリクエストで起きたか分かる

ログに残してはいけない情報

一方で、ログに残すべきではない情報もあります。

  • パスワード
  • クレジットカード番号
  • セキュリティコード
  • アクセストークン
  • APIキー
  • 個人情報
  • セッションID
  • 認証情報

ログは開発者や運用担当者が確認するものですが、保管場所や権限管理によっては漏えいリスクもあります。

そのため、機密情報をそのままログに出力しないよう注意が必要です。

例外処理の悪い書き方

例外を握りつぶす

悪い例です。

try {
    riskyProcess();
} catch (Exception $e) {
    // 何もしない
}

このように、例外を捕捉したあと何もしない書き方は避けるべきです。

エラーが発生しても、開発者や運用担当者が気づけなくなります。

最低でもログを残すようにしましょう。

try {
    riskyProcess();
} catch (Exception $e) {
    error_log($e->getMessage());
}

広すぎるcatchを乱用する

try {
    process();
} catch (Throwable $e) {
    echo 'エラー';
}

Throwable は便利ですが、あらゆる例外やエラーを捕捉します。

そのため、業務ロジックの中で安易に使いすぎると、バグの発見が遅れることがあります。

よい例です。

try {
    process();

} catch (ValidationException $e) {
    echo $e->getMessage();

} catch (PaymentException $e) {
    error_log($e->getMessage());

    echo '決済に失敗しました';

} catch (Throwable $e) {
    error_log($e->getMessage());

    echo '予期しないエラーが発生しました';
}

具体的に処理できる例外は個別に捕捉し、最後の保険として Throwable を使うと整理しやすくなります。

ユーザーに詳細なエラーを表示する

悪い例です。

catch (PDOException $e) {
    echo $e->getMessage();
}

本番環境でこのような書き方をすると、内部情報が表示される可能性があります。

よい例です。

catch (PDOException $e) {
    error_log($e->getMessage());

    echo 'データベース処理に失敗しました';
}

開発環境では詳細を表示し、本番環境では一般的なメッセージにする、という切り替えが重要です。

通常の分岐をすべて例外にする

悪い例です。

try {
    if ($age < 20) {
        throw new Exception('未成年です');
    }

    echo '購入できます';

} catch (Exception $e) {
    echo $e->getMessage();
}

この程度の条件分岐であれば、通常の if 文で十分です。

if ($age < 20) {
    echo '未成年です';
} else {
    echo '購入できます';
}

例外は、通常の処理フローから外れた問題を扱うために使うのが基本です。

例外処理の設計ポイント

どこでcatchするかを決める

例外は、発生した場所ですぐに捕捉すればよいとは限りません。

下位の関数では例外を投げるだけにして、上位のコントローラーやエントリーポイントでまとめて捕捉する設計もよく使われます。

function findUser(int $id): array
{
    if ($id <= 0) {
        throw new InvalidArgumentException('IDが不正です');
    }

    // DB検索処理
}

上位でまとめて処理します。

try {
    $user = findUser($id);

} catch (InvalidArgumentException $e) {
    http_response_code(400);

    echo '不正なリクエストです';

} catch (UserNotFoundException $e) {
    http_response_code(404);

    echo 'ユーザーが見つかりません';
}

下位レイヤーで画面表示やHTTPレスポンスまで行うと、コードの再利用性が下がります。

そのため、どの層で例外を処理するかを決めておくことが大切です。

ユーザー向けメッセージとログを分ける

例外処理では、ユーザー向けのメッセージと開発者向けのログを分けることが重要です。

ユーザー向けには、分かりやすく安全なメッセージを表示します。

echo '処理に失敗しました。時間をおいて再度お試しください。';

開発者向けには、原因調査に必要な情報をログに残します。

error_log($e->getMessage());
error_log($e->getTraceAsString());

ユーザーに詳細を見せすぎると危険です。

一方で、ログが少なすぎると原因調査が難しくなります。

この2つのバランスを取ることが、実務では重要です。

例外クラス名で意味が分かるようにする

独自例外を作る場合は、クラス名だけで意味が伝わるようにしましょう。

よい例です。

class UserNotFoundException extends Exception {}
class PaymentFailedException extends Exception {}
class InvalidCouponException extends Exception {}

悪い例です。

class MyException extends Exception {}
class ErrorException2 extends Exception {}
class CustomException extends Exception {}

MyExceptionCustomException では、何が起きたのか分かりません。

例外クラス名は、エラーの内容が伝わる名前にすることが大切です。

本番環境では詳細を表示しない

本番環境で、次のような情報を画面に表示するのは避けましょう。

echo $e->getFile();
echo $e->getLine();
echo $e->getTraceAsString();

これらは、開発者にとっては有用な情報です。

しかし、ユーザーに見せると、アプリケーションの内部構造が分かってしまう可能性があります。

本番環境では、詳細はログに残し、画面には一般的なメッセージを表示するのが基本です。

PHPの例外処理の実務テンプレート

通常のWebページ向けテンプレート

<?php

try {
    // メイン処理

} catch (InvalidArgumentException $e) {
    http_response_code(400);

    echo '入力内容に誤りがあります';

} catch (RuntimeException $e) {
    error_log($e->getMessage());

    http_response_code(500);

    echo '処理を完了できませんでした';

} catch (Throwable $e) {
    error_log(sprintf(
        "[%s] %s in %s:%d\n%s",
        get_class($e),
        $e->getMessage(),
        $e->getFile(),
        $e->getLine(),
        $e->getTraceAsString()
    ));

    http_response_code(500);

    echo 'システムエラーが発生しました';
}

このテンプレートでは、入力値エラー、実行時エラー、予期しないエラーを分けています。

最終的な catch (Throwable $e) は、予期しないエラーに対する保険として使っています。

JSON API向けテンプレート

<?php

header('Content-Type: application/json; charset=utf-8');

try {
    // メイン処理

    echo json_encode([
        'success' => true,
        'data' => $data,
    ], JSON_UNESCAPED_UNICODE);

} catch (InvalidArgumentException $e) {
    http_response_code(400);

    echo json_encode([
        'success' => false,
        'message' => $e->getMessage(),
    ], JSON_UNESCAPED_UNICODE);

} catch (Throwable $e) {
    error_log(sprintf(
        "[%s] %s in %s:%d\n%s",
        get_class($e),
        $e->getMessage(),
        $e->getFile(),
        $e->getLine(),
        $e->getTraceAsString()
    ));

    http_response_code(500);

    echo json_encode([
        'success' => false,
        'message' => 'サーバーエラーが発生しました',
    ], JSON_UNESCAPED_UNICODE);
}

APIでは、成功時も失敗時もレスポンス形式を統一すると、フロントエンド側で扱いやすくなります。

PHPの例外処理を使うときの注意点

例外を使いすぎない

例外処理は便利ですが、すべての条件分岐を例外で表現する必要はありません。

ユーザー操作として自然に起こるものは、通常の分岐で扱った方が分かりやすい場合もあります。

例外は、通常の処理を続けるのが難しい異常系に使うと効果的です。

例外を握りつぶさない

例外を捕捉したら、何らかの対応を行いましょう。

  • ログを残す
  • ユーザーにメッセージを表示する
  • ロールバックする
  • 再スローする
  • 別の例外に変換する

何もしない catch は、原因不明の不具合につながります。

catchの範囲を広げすぎない

catch (Throwable $e) は便利ですが、広すぎる捕捉です。

基本的には、具体的に処理できる例外を先に捕捉し、最後の保険として Throwable を使うとよいです。

catch (ValidationException $e) {
    // 入力エラー
} catch (PaymentException $e) {
    // 決済エラー
} catch (Throwable $e) {
    // 予期しないエラー
}

ログと表示メッセージを分ける

例外メッセージをそのまま画面に表示すると、内部情報が漏れる可能性があります。

本番環境では、ユーザーには一般的なメッセージを表示し、詳細はログに残しましょう。

error_log($e->getMessage());

echo 'エラーが発生しました。時間をおいて再度お試しください。';

まとめ

PHPの例外処理は、エラーや想定外の状態を安全に扱うための仕組みです。

基本構文は次のとおりです。

try {
    // 例外が発生する可能性のある処理
} catch (Exception $e) {
    // 例外発生時の処理
} finally {
    // 必ず実行する後処理
}

PHPの例外処理では、次のポイントを押さえておくことが重要です。

  • 例外は throw で発生させる
  • try に例外が発生する可能性のある処理を書く
  • catch で例外を受け取って処理する
  • finally は通常、成功・失敗に関係なく実行される
  • Exception はアプリケーション例外の基本クラス
  • ThrowableExceptionError の両方を扱える
  • 独自例外クラスを作ると、エラーの意味を明確にできる
  • DB処理ではトランザクションと例外処理を組み合わせることが多い
  • PHP 8.0以降ではPDOのエラーモードはデフォルトで例外モード
  • 本番環境では詳細なエラーを画面に表示しない
  • 例外を握りつぶさず、ログやロールバックなど適切な対応を行う

例外処理は、単にエラーを捕まえるための仕組みではありません。

正常系と異常系を分け、保守しやすく、安全なコードを書くための設計手法です。

PHPでWebアプリケーションやAPIを開発する場合、例外処理を適切に使うことで、エラーに強く、運用しやすいコードを書けるようになります。

以上、PHPの例外処理についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次