PHPでは、失敗するかもしれないコードをtry { ... }の中に入れ、失敗をcatch (Exception $e) { ... }で処理します。tryブロックの中で何かが例外を投げると、PHPは一致するcatchへまっすぐ飛び、$e->getMessage()で何が問題だったかがわかります。何も投げられなければ、catchブロックは飛ばされます。
divide(10, 0)をdivide(10, 5)に変えてもう一度実行してください。例外がないのでnot printedが出力され、catchブロックは飛ばされます。tryがなければ、例外は「Uncaught」の致命的エラーでスクリプトを終わらせてしまいます。自分で例外を投げる方法や例外クラスの書き方は例外のページにあります。
try catchの構文
完全な形には3つの部分があります。必須なのはtryだけで、それに加えて少なくとも1つのcatchかfinallyが必要です。
try {
// code that may throw
} catch (SomeException $e) {
// runs if SomeException (or a subclass) was thrown
} catch (OtherException | ThirdException $e) {
// runs for either of these types
} finally {
// always runs, thrown or not
}
catchの型が、何をキャッチするかを決めます。そのクラス自身と、それを継承するすべてのクラスです。PHP 8.0以降、変数が不要なら省略できます:catch (JsonException) { ... }。
Exception、Error、Throwableのキャッチ
PHP 7以降、PHP自身が検出する失敗(存在しないメソッドの呼び出し、間違った引数の型、ゼロ除算)はExceptionではなくErrorを投げ、PHP 8では以前の警告の多くがTypeErrorとValueErrorに変わりました。どちらもThrowableインターフェースを実装していますが、どちらももう一方を継承していないので、catch (Exception $e)はErrorをキャッチしません。
Throwable
├── Exception (problems a program expects and handles)
│ ├── InvalidArgumentException, RuntimeException, JsonException, ...
└── Error (engine errors, usually a bug in the code)
├── TypeError
├── ValueError
├── ArithmeticError
│ └── DivisionByZeroError
└── UnhandledMatchError, ...
これを実行すると、それぞれの問題をどのcatchが処理するかがわかります。
では、どれをキャッチすべきでしょうか。
catch (SomeSpecificException $e):何が問題になりうるか、それにどう対処するかがわかっているとき。ほとんどのコードはこれです。catch (Exception $e):「ライブラリや自分のコードが意図的に投げたもの何でも」。catch (Throwable $e):いちばん外側(リクエストハンドラー、ジョブの実行役)で、PHP自身のエラーも含めたすべての失敗をログに残し、親切なメッセージを表示するとき。
複数の例外の型をキャッチする
異なる型を異なる方法で処理するには、複数のcatchブロックを書きます。PHPは上から順に試して最初に一致したものを使うので、具体的な型はその親より前に置く必要があります。そうしないと親が先にすべてをキャッチしてしまいます。複数の型を同じように扱うなら、1つのcatchの中で|でまとめます。
3つのクラスはすべてExceptionを継承しているので、1つのcatch (Exception $e)でもキャッチできますが、そうすると1つには404、ほかには400と答え分けられなくなります。
finally:常に実行されるコード
finallyブロックは、tryとcatchがどう終わっても、そのあとに実行されます。普通に終わっても、キャッチされた例外でも、誰もキャッチしなかった例外でも、returnでもです。tryブロックが確保したものを解放する場所で、ファイルを閉じる、ロックを解放する、タイマーを止める、などに使います。
good.txtでは、finallyが関数が実際に戻る前に実行されるので、savedが出力される前にファイルが閉じられます。bad.txtではprocess()の中にcatchがないので、finallyがファイルを閉じたあと、例外は呼び出し側のcatchまで上がっていきます。
finallyの中のreturnは避けましょう。tryブロックが返した値を置き換え、外へ向かっていた例外を黙って捨ててしまいます。
function f(): string
{
try {
return 'from try';
} finally {
return 'from finally';
}
}
function g(): string
{
try {
throw new RuntimeException('lost');
} finally {
return 'no exception reaches the caller';
}
}
echo f(); // from finally
echo g(); // no exception reaches the caller
例外を再スローする、包む
catchブロックが問題を一部しか処理できないことがあります。ログに残し、後片付けをして、そのまま先へ進ませるのです。throw $e;は同じ例外を再スローします。より多いのは、元の原因を残しつつ文脈を加えたい場合で、そのためにあるのがコンストラクタの3つ目の引数$previousです。
呼び出し側は1つのConfigExceptionを扱えばよく、設定がJSONであることを知る必要はありません。一方でgetPrevious()は、ログのために低レベルの詳細を保ちます。
キャッチされない例外とグローバルなハンドラー
どのcatchも処理しない例外は、クラス、メッセージ、ファイルと行、スタックトレースを含む致命的エラーでスクリプトを終わらせます。
<?php
// No try/catch anywhere in this file
function charge(int $cents): void
{
if ($cents <= 0) {
throw new DomainException("Amount must be positive, got $cents");
}
}
charge(500);
echo "first charge ok\n";
charge(-1);
echo "never printed\n";
PHPはfirst charge okを出力したあと、次のように表示します。
PHP Fatal error: Uncaught DomainException: Amount must be positive, got -1 in /home/index.php:6
Stack trace:
#0 /home/index.php(12): charge()
#1 {main}
thrown in /home/index.php on line 6
公開中のサイトでは、このメッセージが訪問者に届いてはいけません。set_exception_handler()は、誰もキャッチしなかった例外を受け取る関数を登録するもので、ログに残して丁寧なページを出力する1か所になります。
ハンドラーが実行されたあとスクリプトは止まり、throwのあとから再開する方法はありません。PHP自身のエラーの文章をそもそもページに表示するかどうかはdisplay_errorsで制御され、エラー表示で扱っています。
警告は例外ではない
try/catchが見るのは投げられたものだけです。古いPHPの関数の多くは例外を投げず、falseやnullを返し、せいぜい警告を表示するだけです。json_decode()は典型的な例で、例外を投げるよう指示するまでは、tryで包んでも何も変わりません。
そのようなフラグがない関数では、戻り値を確認する(if ($handle === false))か、先に前提条件を確認します(file_exists()、isset())。すべてに1つのルールを適用したいなら、ErrorExceptionを投げるset_error_handler()ですべての警告を本物の例外に変えられます。
レシピ:例外でフォームの入力を検証する
実用的なパターンです。検証関数が例外を投げ、ページがそれをキャッチしてフォームの隣にメッセージを表示します。実行してからフォームに年齢を入力してSendを押すと、$_POSTに値が入った状態で同じスクリプトがもう一度実行されます。
空の欄、twelve、9、30を試してください。失敗したルールはそれぞれ自分のメッセージ付きで例外を投げ、1つのcatchがそのすべてを同じ赤い行に変えます。フォームのページでは、これをもとに複数の項目と項目ごとのエラーを扱います。
よくある質問
PHPのtry catchはどう動きますか?
PHPはtry { }の中のコードを実行します。その中で何かが例外を投げると、PHPはtryブロックの残りを飛ばし、型が一致する最初のcatchブロックを、投げられたオブジェクトを変数に入れて実行します:catch (Exception $e) { echo $e->getMessage(); }。何も投げられなければ、catchブロックは飛ばされます。
PHPですべての例外とエラーをキャッチするには?
Throwableをキャッチします:catch (Throwable $e)。catch (Exception $e)がキャッチするのは例外だけで、Errorを継承したTypeError、ValueError、DivisionByZeroErrorのようなエンジンのエラーはキャッチしません。ExceptionとErrorはどちらもThrowableを実装しています。
PHPで1つのcatchブロックで複数の例外をキャッチするには?
型をパイプで区切ります:catch (InvalidArgumentException | RangeException $e)。複数のcatchブロックを続けて書くこともでき、PHPは最初に一致したものを使うので、具体的な型を先に置きます。
tryの中にreturnがあってもfinallyは実行されますか?
実行されます。finallyは、コードがreturnしても、例外を投げても、普通に終わっても、tryとcatchのあとに実行され、tryにreturnがある場合も同じです。finally自体が値を返すと、その値がtryの値を置き換えるので、finallyの中のreturnは避けましょう。
PHPのtry catchは警告をキャッチしますか?
しません。警告や通知(存在しない配列キーの読み取りなど)は例外ではないので、catchには決して届きません。呼び出す前に条件を確認するか、関数に例外モードがあればそれを使う(json_decode(..., flags: JSON_THROW_ON_ERROR))か、set_error_handler()とErrorExceptionで警告を変換します。