Menu

PHPのtry catch:例外のキャッチ、finally、Throwable

PHPでは、失敗するかもしれないコードをtry { }ブロックに入れ、例外が投げられるとcatch (Exception $e) { }が実行され、理由は$e->getMessage()でわかります。複数の例外の型のキャッチ、finally、catch (Exception)がTypeErrorやDivisionByZeroErrorを取りこぼす理由(Throwableをキャッチする)、再スロー、グローバルなハンドラーを学びます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

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で警告を変換します。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める