- PHP
PHP例外処理 try catch finally|独自例外とErrorの違い
PHPの例外処理とは、処理の途中で起きた問題を throw で知らせ、try で囲んだ範囲の中で起きたものを catch で受け取り、finally で後始末をする仕組みです。 受け取れるのは Throwable というインターフェースを持つオブジェクトで、その下に Exception 系と Error 系の2つの系統があります。
入門記事の多くは Exception だけを扱いますが、PHP 7 以降では型の不一致やゼロ除算は Error 系として投げられるため、catch (Exception $e) では受け取れません。この記事では、すべてのコードを PHP 8.4.21 で実行し、その出力をそのまま載せています。 「どの例外がどちらの系統か」「finally と return はどちらが先か」といった迷いやすい点を、実行結果で確かめながら整理します。
PHP例外処理の早見表
構文の一覧です。対応バージョンのうち、複数 catch・変数なし catch・throw 式の3つは PHP マニュアルの「例外」のページを今回開いて確認しました。それ以外は一般に知られた仕様です。
| やりたいこと | 書き方 | 使えるバージョン |
|---|---|---|
| 例外を投げる | throw new InvalidArgumentException('メッセージ'); |
PHP 5〜 |
| 例外を受け取る | try { ... } catch (InvalidArgumentException $e) { ... } |
PHP 5〜 |
| 必ず後始末をする | finally { ... } |
PHP 5.5〜 |
| Error も Exception もまとめて受け取る | catch (Throwable $e) |
PHP 7.0〜 |
| 複数の型を1つの catch で受け取る | catch (JsonException | TypeError $e) |
PHP 7.1〜 |
| 変数を使わずに受け取る | catch (JsonException) { ... } |
PHP 8.0〜 |
| 式の中で投げる(throw 式) | $v = $a['k'] ?? throw new Exception('必須'); |
PHP 8.0〜 |
| 元の例外を保ったまま包む | throw new MyException('説明', 0, $e); |
PHP 5.3〜 |
| 独自の例外を作る | class MyException extends RuntimeException {} |
PHP 5〜 |
| 警告を例外に変える | set_error_handler() で ErrorException を投げる |
PHP 5.1〜 |
| 受け取られなかった例外の受け皿 | set_exception_handler() |
PHP 5〜 |
Error と Exception の違いは次のとおりです。
| 比べる点 | Exception 系 | Error 系 |
|---|---|---|
| 主な発生元 | 自分のコードやライブラリが throw する | PHP 本体が投げる(型の不一致、ゼロ除算、未定義の関数など) |
| 意味 | 入力や外部の状態によって起こりうる、想定できる問題 | コードの書き方に原因があることが多い問題 |
catch (Exception $e) |
受け取れる | 受け取れない |
catch (Throwable $e) |
受け取れる | 受け取れる |
| 代表例 | InvalidArgumentException、JsonException、PDOException |
TypeError、DivisionByZeroError、ValueError |
| 基本の方針 | 対処できる場所で catch する | コードを直す。最上位でまとめて記録する |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「PHP例外処理 try catch finally|独自例外とErrorの違い」
try・catch・finallyの基本の書き方
まずは最小の形です。備品の在庫数を調べ、登録がなければ例外を投げます。
<?php
declare(strict_types=1);
function findItem(array $stock, string $name): int
{
if (!array_key_exists($name, $stock)) {
throw new InvalidArgumentException("在庫に {$name} がありません");
}
return $stock[$name];
}
$stock = ['pen' => 12, 'note' => 0];
foreach (['pen', 'desk'] as $name) {
try {
echo "{$name}: " . findItem($stock, $name) . " 個\n";
} catch (InvalidArgumentException $e) {
echo "catch: " . $e->getMessage() . "\n";
} finally {
echo "finally: {$name} の確認を終了\n";
}
}
実行結果(PHP 8.4.21):
pen: 12 個
finally: pen の確認を終了
catch: 在庫に desk がありません
finally: desk の確認を終了
流れを表にすると次のようになります。
| ブロック | 実行されるとき | 役割 |
|---|---|---|
try |
常に | 例外が起こりうる処理を囲む |
catch |
try の中で、指定した型の例外が投げられたとき | 例外を受け取って対処する |
finally |
例外があってもなくても、最後に必ず | ファイルを閉じる、ロックを外すなどの後始末 |
例外が投げられると、try の中の残りの行は飛ばされて、型が合う catch へ移ります。 上の例では、desk のとき echo "{$name}: ..." は出力されていません。
例外オブジェクトから取り出せる情報は次のとおりです。
| メソッド | 取り出せるもの |
|---|---|
getMessage() |
メッセージ |
getCode() |
コード(整数。省略時は 0) |
getPrevious() |
包まれている元の例外(なければ null) |
getFile() / getLine() |
例外が作られたファイルと行 |
getTrace() / getTraceAsString() |
呼び出しの経路(スタックトレース) |
Throwableの階層図とErrorとExceptionの違い
throw できるのは、Throwable インターフェースを持つオブジェクトだけです。Throwable の下に Error と Exception があり、それぞれにさらに細かいクラスがぶら下がっています。
classDiagram
Throwable <|-- Error
Throwable <|-- Exception
Error <|-- TypeError
TypeError <|-- ArgumentCountError
Error <|-- ValueError
Error <|-- ArithmeticError
ArithmeticError <|-- DivisionByZeroError
Error <|-- UnhandledMatchError
Error <|-- CompileError
CompileError <|-- ParseError
Error <|-- AssertionError
Exception <|-- ErrorException
Exception <|-- JsonException
Exception <|-- LogicException
Exception <|-- RuntimeException
LogicException <|-- InvalidArgumentException
LogicException <|-- DomainException
LogicException <|-- LengthException
LogicException <|-- OutOfRangeException
RuntimeException <|-- OutOfBoundsException
RuntimeException <|-- RangeException
RuntimeException <|-- UnexpectedValueException
RuntimeException <|-- PDOException
class Throwable {
<<interface>>
}
この図は、PHP 8.4.21 で宣言済みのクラスを親子関係でたどった結果から、よく使うものを抜き出したものです。ほかにも DateError、FiberError、Random\RandomError(Error 系)、DateException、ReflectionException、RequestParseBodyException(Exception 系)などがあります。
どの例外がいつ発生するかを実行して確かめる
次のコードは、いろいろな問題をわざと起こし、まず catch (Exception $e) で受け、受け取れなかったものを外側の catch (Error $e) で受けて、どちらで捕まったかを表示します。
<?php
declare(strict_types=1);
function half(int $n): int { return intdiv($n, 2); }
$cases = [
'TypeError' => fn() => half("10"),
'DivisionByZeroError' => fn() => 10 % 0,
'DivisionByZeroError(/)' => fn() => 10 / 0,
'ArgumentCountError' => fn() => half(),
'ValueError' => fn() => str_repeat('a', -1),
'Error(未定義関数)' => fn() => no_such_function(),
'Error(nullのメソッド)' => fn() => null->run(),
'UnhandledMatchError' => fn() => match (5) { 1 => 'a' },
'JsonException' => fn() => json_decode('{bad', flags: JSON_THROW_ON_ERROR),
'ParseError' => fn() => eval('echo 1 +;'),
'ArithmeticError' => fn() => intdiv(PHP_INT_MIN, -1),
'DateMalformedStringException' => fn() => new DateTime('not a date'),
'RangeException等(自分でthrow)' => fn() => throw new RangeException('範囲外'),
];
foreach ($cases as $label => $fn) {
try {
try {
$fn();
echo "{$label}: 例外なし\n";
} catch (Exception $e) {
echo "[Exceptionで捕捉] " . get_class($e) . ": " . $e->getMessage() . "\n";
}
} catch (Error $e) {
echo "[Exceptionでは拾えずErrorで捕捉] " . get_class($e) . ": " . $e->getMessage() . "\n";
}
}
実行結果(PHP 8.4.21。ファイルのパスは環境に合わせて置き換えています):
[Exceptionでは拾えずErrorで捕捉] TypeError: half(): Argument #1 ($n) must be of type int, string given, called in /home/taro/app/02_error.php on line 7
[Exceptionでは拾えずErrorで捕捉] DivisionByZeroError: Modulo by zero
[Exceptionでは拾えずErrorで捕捉] DivisionByZeroError: Division by zero
[Exceptionでは拾えずErrorで捕捉] ArgumentCountError: Too few arguments to function half(), 0 passed in /home/taro/app/02_error.php on line 10 and exactly 1 expected
[Exceptionでは拾えずErrorで捕捉] ValueError: str_repeat(): Argument #2 ($times) must be greater than or equal to 0
[Exceptionでは拾えずErrorで捕捉] Error: Call to undefined function no_such_function()
[Exceptionでは拾えずErrorで捕捉] Error: Call to a member function run() on null
[Exceptionでは拾えずErrorで捕捉] UnhandledMatchError: Unhandled match case of type int
[Exceptionで捕捉] JsonException: Syntax error
[Exceptionでは拾えずErrorで捕捉] ParseError: syntax error, unexpected token ";"
[Exceptionでは拾えずErrorで捕捉] ArithmeticError: Division of PHP_INT_MIN by -1 is not an integer
[Exceptionで捕捉] DateMalformedStringException: Failed to parse time string (not a date) at position 0 (n): The timezone could not be found in the database
[Exceptionで捕捉] RangeException: 範囲外
13件のうち、catch (Exception $e) で受け取れたのは3件だけでした。結果を「いつ発生するか」の一覧にまとめます。
| クラス | 系統 | いつ発生するか | 上の実行でのメッセージ |
|---|---|---|---|
TypeError |
Error | 引数や戻り値の型が宣言と合わない | must be of type int, string given |
ArgumentCountError |
Error(TypeError の子) | 引数の数が足りない | Too few arguments to function half() |
ValueError |
Error | 型は合っているが値が受け付けられない | must be greater than or equal to 0 |
DivisionByZeroError |
Error(ArithmeticError の子) | 0 で割る(/、%、intdiv()) |
Division by zero / Modulo by zero |
ArithmeticError |
Error | 整数で表せない計算結果 | Division of PHP_INT_MIN by -1 is not an integer |
Error |
Error | 未定義の関数を呼ぶ、null のメソッドを呼ぶ | Call to undefined function / Call to a member function run() on null |
UnhandledMatchError |
Error | match 式でどの条件にも当てはまらない | Unhandled match case of type int |
ParseError |
Error(CompileError の子) | eval() や include したコードに構文の誤りがある |
syntax error, unexpected token ";" |
JsonException |
Exception | JSON_THROW_ON_ERROR をつけた JSON の変換で問題がある |
Syntax error |
DateMalformedStringException |
Exception(DateException の子) | 日付として解釈できない文字列を渡す | Failed to parse time string |
RangeException など |
Exception | 自分のコードやライブラリが throw する | 範囲外 |
PHP 本体が「コードの書き方」に対して投げるものは Error 系、入力データや外部の状態に対して投げるものは Exception 系、という分かれ方になっています。
catch (Exception $e) で受け取れないと処理はどうなるか
Error 系を catch (Exception $e) だけで囲んだ場合の動きです。
<?php
try {
echo intdiv(10, 0);
} catch (Exception $e) {
echo "ここには来ません\n";
}
echo "この行も実行されません\n";
実行結果:
PHP Fatal error: Uncaught DivisionByZeroError: Division by zero in /home/taro/app/03_uncaught.php:3
Stack trace:
#0 /home/taro/app/03_uncaught.php(3): intdiv()
#1 {main}
thrown in /home/taro/app/03_uncaught.php on line 3
catch を素通りし、「Uncaught(受け取られなかった)」として Fatal error になり、スクリプトはそこで止まりました。 終了コードは 255 でした。
直し方は、目的に応じて3つあります。
| 目的 | 書き方 |
|---|---|
| ゼロ除算だけを受けたい | catch (DivisionByZeroError $e) |
| Error 系も Exception 系もまとめて受けたい | catch (Throwable $e) |
| そもそも起こさない | 割る前に if ($n === 0) で確かめる |
<?php
try {
echo intdiv(10, 0);
} catch (DivisionByZeroError $e) {
echo "0 では割れません\n";
}
Error 系は、受け取って続行するより、原因のコードを直すのが基本です。catch (Throwable $e) は、アプリケーションの最上位で記録を残す場面に向いています。
名前空間の中では先頭に \ をつける
もう1つ、catch を素通りする原因があります。名前空間(namespace)を宣言したファイルで、Exception を先頭の \ なしで書いた場合です。
<?php
namespace App\Demo;
try {
throw new \RuntimeException('x');
} catch (Exception $e) {
echo "catch できました\n";
}
実行結果:
PHP Fatal error: Uncaught RuntimeException: x in /home/taro/app/nf1b.php:4
catch (Exception $e) は App\Demo\Exception という存在しないクラスを指すことになり、エラーも出ないまま素通りします。名前空間の中では catch (\Exception $e) と書くか、ファイルの先頭に use Exception; を書きます。
finallyとreturnの実行順
finally は「必ず実行される」ブロックですが、try や catch の中に return があるときの順番は迷いやすいところです。5つの形をまとめて実行します。
<?php
function a(): string {
try {
echo "1. try\n";
return "tryの戻り値";
} finally {
echo "2. finally\n";
}
}
echo "3. " . a() . "\n\n";
function b(): string {
try {
throw new RuntimeException('x');
} catch (RuntimeException $e) {
return "catchの戻り値";
} finally {
return "finallyの戻り値";
}
}
echo "b(): " . b() . "\n";
function c(): string {
try {
throw new RuntimeException('この例外は外へ出ません');
} finally {
return "finallyのreturnが例外を上書き";
}
}
echo "c(): " . c() . "\n";
function d(): int {
$n = 1;
try {
return $n;
} finally {
$n = 99;
}
}
echo "d(): " . d() . "\n\n";
function e(): void {
try {
echo "try\n";
throw new LogicException('未捕捉');
} finally {
echo "finally(catchがなくても実行)\n";
}
}
try { e(); } catch (LogicException $e) { echo "外側でcatch: " . $e->getMessage() . "\n"; }
実行結果:
1. try
2. finally
3. tryの戻り値
b(): finallyの戻り値
c(): finallyのreturnが例外を上書き
d(): 1
try
finally(catchがなくても実行)
外側でcatch: 未捕捉
| 関数 | 形 | 結果 | わかること |
|---|---|---|---|
| a | try で return、finally で出力 | try → finally → 呼び出し元の順 | return の値は先に決まり、finally を実行してから呼び出し元へ戻る |
| b | catch と finally の両方で return | finally の値が返る | finally の return が優先される |
| c | 例外が出たまま finally で return | 例外が消えて値が返る | finally の return は投げられた例外を打ち消す |
| d | try で $n を return、finally で $n を書き換え |
1 が返る | return の値は return の時点で確定している |
| e | catch なしの try-finally | finally のあと例外が外へ | catch がなくても finally は動く |
PHP マニュアルにも、return 文はその時点で評価され、finally の実行後に結果が返される、と書かれています。
c の動きには注意が要ります。例外が起きたことが呼び出し元に伝わらなくなるため、finally には return を書かず、後始末だけを書くのがおすすめです。
複数catchと書く順番
型ごとに対処を分ける・まとめる
catch は複数並べられます。同じ対処でよい型は | でまとめられます。
<?php
declare(strict_types=1);
function parseQuantity(string $raw): int
{
$data = json_decode($raw, true, flags: JSON_THROW_ON_ERROR);
return intdiv(100, $data['qty']);
}
foreach (['{"qty":4}', '{bad', '{"qty":0}', '{"qty":"x"}'] as $raw) {
try {
echo parseQuantity($raw) . "\n";
} catch (JsonException | TypeError $e) {
echo "入力の形式: " . get_class($e) . "\n";
} catch (DivisionByZeroError) {
echo "数量が0です\n";
}
}
実行結果:
25
入力の形式: JsonException
数量が0です
入力の形式: TypeError
JsonException(Exception 系)と TypeError(Error 系)のように、系統が違う型も | で1つにまとめられます。 2つめの catch は変数を書かない形(PHP 8.0 以降)です。例外オブジェクトを使わないときは、この形にすると意図が伝わりやすくなります。
親クラスを先に書くと子クラスの catch に届かない
catch は上から順に型を比べ、最初に合ったものだけが実行されます。
<?php
try {
throw new InvalidArgumentException('引数が不正');
} catch (Exception $e) {
echo "Exception側: " . get_class($e) . "\n";
} catch (InvalidArgumentException $e) {
echo "InvalidArgumentException側\n";
}
実行結果:
Exception側: InvalidArgumentException
InvalidArgumentException は Exception の子なので、先に書いた catch (Exception $e) に合ってしまい、2つめには届きません。エラーや警告も出ません。catch は「細かい型を上、広い型を下」の順に並べます。
| 並べる順 | 例 |
|---|---|
| 1(上) | catch (OutOfStockException $e) 自分で作った具体的な型 |
| 2 | catch (InvalidArgumentException $e) 標準の具体的な型 |
| 3 | catch (RuntimeException $e) 親の型 |
| 4(下) | catch (Throwable $e) すべて |
独自例外クラスの作り方
独自例外は、Exception かその子クラスを継承して作ります。最小の形はクラス名を決めるだけです。
class ItemNotFoundException extends RuntimeException {}
これだけでも「型で区別して catch できる」という大きな利点があります。追加の情報を持たせたいときは、コンストラクタでプロパティを受け取ります。
<?php
declare(strict_types=1);
// アプリ共通の親
class AppException extends RuntimeException {}
// 追加情報を持つ独自例外
final class OutOfStockException extends AppException
{
public function __construct(
public readonly string $itemName,
public readonly int $requested,
public readonly int $available,
?Throwable $previous = null,
) {
parent::__construct(
"{$itemName} の在庫が足りません(希望 {$requested} / 在庫 {$available})",
1001,
$previous
);
}
}
final class ItemNotFoundException extends AppException {}
function reserve(array $stock, string $name, int $qty): int
{
$available = $stock[$name] ?? throw new ItemNotFoundException("{$name} は登録されていません");
if ($available < $qty) {
throw new OutOfStockException($name, $qty, $available);
}
return $available - $qty;
}
$stock = ['pen' => 3];
foreach ([['pen', 2], ['pen', 5], ['desk', 1]] as [$name, $qty]) {
try {
echo "残り " . reserve($stock, $name, $qty) . "\n";
} catch (OutOfStockException $e) {
echo "在庫不足: {$e->itemName} あと " . ($e->requested - $e->available) . " 個必要 (code={$e->getCode()})\n";
} catch (AppException $e) {
echo get_class($e) . ": {$e->getMessage()}\n";
}
}
実行結果:
残り 1
在庫不足: pen あと 2 個必要 (code=1001)
ItemNotFoundException: desk は登録されていません
独自例外を作るときのポイントです。
| ポイント | 理由 |
|---|---|
アプリ共通の親クラス(ここでは AppException)を1つ作る |
「自分のアプリの例外」をまとめて catch できる |
必ず parent::__construct() を呼ぶ |
メッセージ・コード・previous が正しく設定される |
追加情報は public readonly のプロパティで持つ |
catch した側がメッセージの文字列を分解せずに値を使える |
$previous を引数に用意しておく |
元の例外を包んで渡せる(次の節) |
継承元は Exception 系にする |
利用側の catch (Exception $e) で受け取れる |
なお、Throwable を直接 implements したクラスは作れません。試すと次のメッセージで止まります。
PHP Fatal error: Class My cannot implement interface Throwable, extend Exception or Error instead
メッセージのとおり、Exception か Error を継承します。
標準の例外クラスの選び方
独自例外の親や、そのまま投げる型を選ぶときの目安です。
| クラス | 親 | 向いている場面 |
|---|---|---|
InvalidArgumentException |
LogicException | 引数の値が受け付けられない |
DomainException |
LogicException | 値が決められた種類に含まれない |
LengthException |
LogicException | 長さが決まりに合わない |
OutOfRangeException |
LogicException | 存在しない添字(コードを直せば防げるもの) |
BadMethodCallException |
BadFunctionCallException | 呼べないメソッドを呼んだ |
RuntimeException |
Exception | 実行してみないとわからない問題全般 |
OutOfBoundsException |
RuntimeException | 存在しないキー(実行時のデータによるもの) |
RangeException |
RuntimeException | 計算結果が範囲に収まらない |
UnexpectedValueException |
RuntimeException | 戻り値などが想定した型や値でない |
LogicException 系は「コードを直せば起きなくなる問題」、RuntimeException 系は「実行時の入力や環境で起こりうる問題」と考えると選びやすくなります。
previousで例外をつなげる(例外チェーン)
下の層で起きた例外を、上の層で意味のわかる例外に包み直すことがあります。このとき元の例外を第3引数(previous)に渡すと、原因をたどれる状態のまま包めます。
<?php
declare(strict_types=1);
class ConfigLoadException extends RuntimeException {}
function loadConfig(string $json): array
{
try {
return json_decode($json, true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
// 元の例外を第3引数 previous に渡して包む
throw new ConfigLoadException('設定を読み込めませんでした', 0, $e);
}
}
try {
loadConfig('{"debug": tru}');
} catch (ConfigLoadException $e) {
for ($cur = $e, $i = 0; $cur !== null; $cur = $cur->getPrevious(), $i++) {
echo str_repeat(' ', $i) . get_class($cur) . ': ' . $cur->getMessage()
. ' (' . basename($cur->getFile()) . ':' . $cur->getLine() . ")\n";
}
}
実行結果:
ConfigLoadException: 設定を読み込めませんでした (08_previous.php:12)
JsonException: Syntax error (08_previous.php:9)
getPrevious() をたどると、包む前の JsonException と、それが起きた行(9行目)まで取り出せました。
| 書き方 | 原因の情報 |
|---|---|
throw new ConfigLoadException('設定を読み込めませんでした'); |
元の例外の型・メッセージ・行が残らない |
throw new ConfigLoadException('設定を読み込めませんでした', 0, $e); |
getPrevious() でたどれる |
同じ例外をそのまま上へ渡す(再スロー)
記録だけして、対処は呼び出し元に任せたいときは throw $e; と書きます。
<?php
function step(): void {
try {
throw new RuntimeException('接続が切れました');
} catch (RuntimeException $e) {
echo "ログに記録: {$e->getMessage()}\n";
throw $e; // 同じ例外をそのまま上へ
} finally {
echo "finally: 後始末\n";
}
}
try { step(); } catch (Throwable $e) {
echo "最上位: " . get_class($e) . " line=" . $e->getLine() . "\n";
echo $e->getTraceAsString() . "\n";
}
実行結果(パスは環境に合わせて置き換えています):
ログに記録: 接続が切れました
finally: 後始末
最上位: RuntimeException line=4
#0 /home/taro/app/12_rethrow.php(12): step()
#1 {main}
再スローしても、行番号は最初に例外が作られた4行目のままです。catch の中で throw しても finally は実行され、そのあとで外側の catch に届いています。
PHP 8のthrow式と変数なしcatch
PHP 8.0 から、throw は「文」ではなく「式」になりました。値を書ける場所ならどこにでも置けます。
<?php
declare(strict_types=1);
$input = ['name' => 'pen'];
// ?? と組み合わせる
try {
$price = $input['price'] ?? throw new InvalidArgumentException('price は必須です');
} catch (InvalidArgumentException $e) {
echo "1: {$e->getMessage()}\n";
}
// 三項演算子
try {
$qty = -1;
$checked = $qty >= 0 ? $qty : throw new RangeException('qty は0以上');
} catch (RangeException $e) {
echo "2: {$e->getMessage()}\n";
}
// アロー関数
$fail = fn(string $m) => throw new LogicException($m);
try { $fail('アロー関数からthrow'); } catch (LogicException $e) { echo "3: {$e->getMessage()}\n"; }
// match の default
try {
$status = 'unknown';
$label = match ($status) {
'open' => '受付中',
'done' => '完了',
default => throw new UnexpectedValueException("未知の状態: {$status}"),
};
} catch (UnexpectedValueException $e) {
echo "4: {$e->getMessage()}\n";
}
// 変数なし catch(PHP 8.0〜)
try {
new DateTimeImmutable('2026-13-45');
} catch (Exception) {
echo "5: 日付として解釈できませんでした\n";
}
実行結果:
1: price は必須です
2: qty は0以上
3: アロー関数からthrow
4: 未知の状態: unknown
5: 日付として解釈できませんでした
| 書ける場所 | 例 | 置き換えられる書き方 |
|---|---|---|
?? の右側 |
$a['k'] ?? throw new ... |
if (!isset($a['k'])) { throw ...; } |
| 三項演算子 | $ok ? $v : throw new ... |
if 文 |
| アロー関数の本体 | fn() => throw new ... |
通常の無名関数 |
| match の腕 | default => throw new ... |
match の前後の if 文 |
「値がなければ例外」を1行で書けるので、入力チェックが短くなります。 match では、default に throw を置くことで、標準の UnhandledMatchError の代わりに、意味のわかるメッセージを持つ例外を投げられます。
警告を例外に変えるset_error_handler
PHP には、例外とは別に「警告(Warning)」や「通知(Notice)」という仕組みがあります。たとえば存在しないファイルを file() で開くと、例外ではなく警告が出て、処理はそのまま続きます。
<?php
$lines = file('/no/such/file.txt');
echo "警告のあとも処理は続きます\n";
var_dump($lines);
実行結果:
PHP Warning: file(/no/such/file.txt): Failed to open stream: No such file or directory in /home/taro/app/11_nohandler.php on line 2
警告のあとも処理は続きます
bool(false)
警告は try-catch では受け取れません。set_error_handler() で警告を ErrorException に変換すると、ほかの例外と同じように扱えます。あわせて、どこでも catch されなかった例外の受け皿になる set_exception_handler() も登録します。次のコードは、アプリケーションの入口のファイルの先頭にそのまま貼って使えます。
<?php
declare(strict_types=1);
// ① 警告・通知を ErrorException に変える
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
if (!(error_reporting() & $severity)) {
return false; // @ や error_reporting で抑制されたものは標準の処理に任せる
}
throw new ErrorException($message, 0, $severity, $file, $line);
});
// ② どこでも catch されなかった例外の最終受け皿
set_exception_handler(function (Throwable $e): void {
error_log(sprintf('[未捕捉] %s: %s (%s:%d)', get_class($e), $e->getMessage(), basename($e->getFile()), $e->getLine()));
if (PHP_SAPI !== 'cli') {
http_response_code(500);
}
echo "エラーが発生しました。時間をおいてもう一度お試しください。\n";
});
// --- 動作確認 ---
try {
$lines = file('/no/such/file.txt'); // 通常は Warning
} catch (ErrorException $e) {
echo "catch: " . $e->getMessage() . " (severity=" . $e->getSeverity() . ")\n";
}
try {
$arr = [];
echo $arr['missing']; // 通常は Warning: Undefined array key
} catch (ErrorException $e) {
echo "catch: " . $e->getMessage() . "\n";
}
$r = @file('/no/such/file.txt'); // @ 付きは例外にしない
var_dump($r);
throw new DomainException('どこでも捕まえていない例外');
echo "ここは実行されません\n";
実行結果([未捕捉] の行は error_log の出力先である標準エラーに出たものです):
catch: file(/no/such/file.txt): Failed to open stream: No such file or directory (severity=2)
catch: Undefined array key "missing"
bool(false)
[未捕捉] DomainException: どこでも捕まえていない例外 (10_handler.php:38)
エラーが発生しました。時間をおいてもう一度お試しください。
| 確認できたこと | 説明 |
|---|---|
| 警告が catch できた | file() の警告が ErrorException になった。severity=2 は E_WARNING |
| 未定義のキーも catch できた | 配列の存在しないキーの読み取りも例外になった |
@ をつけた行は例外にならない |
error_reporting() & $severity の判定で標準の処理に戻している |
| 最終受け皿が動いた | catch されなかった DomainException がハンドラに渡り、記録と案内の表示ができた |
| ハンドラのあとは続行しない | 最後の echo は実行されず、スクリプトは終了した |
2つのハンドラの役割の違いです。
| 関数 | 受け取るもの | 役割 |
|---|---|---|
set_error_handler() |
Warning・Notice・Deprecated など(例外ではないもの) | 例外に変換して、try-catch の流れに乗せる |
set_exception_handler() |
どこでも catch されなかった Throwable | 記録を残し、利用者には一般的な案内だけを表示する |
最終受け皿では、例外のメッセージやスタックトレースを画面にそのまま出さず、ログに記録するようにします。内部のファイル構成などが利用者に見えるのを防ぐためです。Laravel などのフレームワークは、同様の仕組みを最初から備えています。
catchするか呼び出し元に任せるかの判断表
「すべての処理を try-catch で囲む」必要はありません。その場で対処できるかどうかで決めます。
| 状況 | おすすめ | 例 |
|---|---|---|
| その場で代わりの値や処理を用意できる | catch して対処する | 設定ファイルが読めなければ既定値を使う |
| 意味のわかる例外に言い換えたい | catch して previous つきで包み直す | JsonException を ConfigLoadException に |
| 記録だけ残したい | catch して記録し、throw $e; で再スロー |
途中の層でログを残す |
| 後始末だけ必要 | try-finally(catch なし) | 一時ファイルの削除、ロックの解放 |
| その場では何もできない | catch しない。呼び出し元に任せる | 下位の関数の中 |
| 型の不一致などコードの誤り(Error 系) | コードを直す。最上位で記録する | TypeError、未定義の関数 |
| アプリケーションの入口 | catch (Throwable $e) か set_exception_handler() |
記録と、利用者向けの案内 |
よくあるつまずきと直し方もまとめます。
| つまずき | 起きること | 直し方 |
|---|---|---|
catch (Exception $e) で TypeError を受けようとする |
Fatal error: Uncaught TypeError で止まる | catch (TypeError $e) か catch (Throwable $e) |
名前空間の中で catch (Exception $e) |
素通りして Uncaught になる | catch (\Exception $e) か use Exception; |
| 親クラスの catch を先に書く | 子クラスの catch に届かない | 細かい型を上に並べる |
| catch の中を空にする | 問題が起きたことが誰にも伝わらない | 記録を残すか、再スローする |
| finally に return を書く | 投げられた例外が消える | finally は後始末だけにする |
| 包み直すときに previous を渡さない | 元の原因がたどれない | 第3引数に $e を渡す |
| 警告を try-catch で受けようとする | 受け取れず処理が続く | set_error_handler() で ErrorException に変える |
よくある質問
Q. Exception と Error はどう使い分けますか?
A. 自分で投げるのは Exception 系です。Error 系は主に PHP 本体が、型の不一致やゼロ除算などコードの書き方に原因がある問題に対して投げます。今回の実行では、13件のうち catch (Exception $e) で受け取れたのは JsonException、DateMalformedStringException、RangeException の3件で、残りは Error 系でした。
Q. finally は return のあとでも実行されますか? A. 実行されます。try の中で return しても、finally を実行してから呼び出し元へ戻ります。戻り値は return の時点で決まっているので、finally で変数を書き換えても戻り値は変わりません。ただし finally の中に return を書くと、その値が優先され、投げられた例外も打ち消されます。
Q. catch (Throwable $e) を常に使えばよいですか? A. アプリケーションの入口で記録を残す用途には向いています。途中の処理で使うと、型の不一致のような「コードを直すべき問題」まで受け取ってしまい、気づきにくくなります。途中の処理では、対処できる具体的な型を指定するのがおすすめです。
Q. 独自例外はどのクラスを継承すればよいですか?
A. 迷ったら RuntimeException を継承します。実行時の入力や環境で起こりうる問題を表す型です。引数の誤りのようにコードを直せば防げる問題なら LogicException 系(InvalidArgumentException など)が合います。アプリ共通の親クラスを1つ作り、そこから派生させるとまとめて catch できます。
まとめ
- throw できるのは Throwable。その下に Exception 系と Error 系があります。
- TypeError・DivisionByZeroError・ValueError などは Error 系で、
catch (Exception $e)では受け取れません。まとめて受けるならcatch (Throwable $e)です。 - finally は return のあとでも実行されます。 finally に return を書くと例外が打ち消されるので、後始末だけを書きます。
- catch は細かい型を上、広い型を下に並べます。 同じ対処の型は
|でまとめられます(PHP 7.1〜)。 - 包み直すときは第3引数に元の例外(previous)を渡すと、
getPrevious()で原因までたどれます。 - PHP 8.0 からは throw を式として書け、catch の変数も省略できます。
- set_error_handler で警告を ErrorException に変え、set_exception_handler を最終受け皿にすると、問題の扱いを例外に一本化できます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「PHP例外処理 try catch finally|独自例外とErrorの違い」
株式会社RJCでは、PHPの例外処理のような基礎の仕組みを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。