Throwable / Error / Exceptionの階層を理解する
Throwableは投げられるものすべての頂点となるインターフェースで、その下に業務例外のExceptionとエンジンレベルのErrorが分かれます。この階層でcatchの基準を決めます。
頂点はThrowableインターフェース
PHP 7 以降、投げられるものすべての頂点に Throwable インターフェースがあります。その下に大きく Exception(アプリケーションの例外)と Error(PHP エンジンレベルの重大エラー)の二系統がぶら下がる構造です。この二本立てを知っておくと、catch の書き方に迷わなくなりますね。
// 階層のイメージ
// Throwable (interface)
// ├── Exception ── RuntimeException, LogicException ...
// └── Error ── TypeError, ValueError, DivisionByZeroError ...
try {
// ...
} catch (Throwable $e) {
// Exception も Error も両方ここで拾える
}
昔は catch (Exception $e) で全部拾えた時代の名残で、Error 系を取りこぼしているコードをたまに見かけます。
ErrorとExceptionの守備範囲が違う
Exception は業務ロジックで意図的に投げる例外、Error は型不一致(TypeError)や存在しない関数の呼び出しなど、本来はコードのバグに近いものです。PHP 8 ではゼロ除算が DivisionByZeroError、不正な引数値が ValueError として Error 系で飛んでくるようになりました。
function half(int $n): int
{
return intdiv($n, 2);
}
try {
half('文字列'); // TypeError (Error 系)
intdiv(1, 0); // DivisionByZeroError (Error 系)
} catch (Exception $e) {
// これでは上記は捕まらない!
}
catch (Exception) だけだと Error 系はすり抜けて致命的エラーになります。ここが移行時にハマりやすいところですね。
catchの基準をどう置くか
基本方針としては、想定内の失敗は具体的な例外型で個別に、最上位のセーフティネットでは Throwable を拾う、という二段構えが扱いやすいです。エントリポイントに Throwable の catch を一つ置いて、そこでログとエラーページ表示をまとめる、というのが私のいつもの形です。
try {
$result = $service->run($request);
} catch (ValidationException $e) {
// 想定内: 具体的な型で丁寧に扱う
return respondBadRequest($e->getMessage());
} catch (Throwable $e) {
// 想定外の Error も含めて最後の砦で受ける
error_log((string)$e);
return respondServerError();
}
まとめ
投げられるものの頂点は Throwable で、その下に業務例外の Exception とエンジンレベルの Error が分かれています。PHP 8 では型エラーやゼロ除算が Error 系で飛ぶので、catch (Exception) だけでは取りこぼします。想定内の失敗は具体型で、最後のセーフティネットは Throwable で受ける。この基準で組んでおけば、想定外のバグも静かに致命的エラーにならずに済むと思います。
よくある質問
Q. catch (Exception) で全部拾えますか。
A. いいえ。TypeErrorやDivisionByZeroErrorなどError系はExceptionではないので取りこぼします。
Q. すべての例外を確実に受けるには。
A. 頂点の Throwable でcatchすると、ExceptionもErrorも両方拾えます。
Q. catchはどう組むのがよいですか。
A. 想定内の失敗は具体的な例外型で個別に、最後のセーフティネットは Throwable で受ける二段構えが扱いやすいです。