Иерархия Throwable
Блок catch (Exception $e) ловит Exception и его дочерние классы. Кажется, что этого хватит на любой сбой. Давайте проверим на ошибке, которую бросает сам PHP:
<?php
function stars(int $count): string
{
try {
return str_repeat('*', $count);
} catch (Exception $e) {
return '';
}
}
echo stars(3), "\n"; // ***
echo stars(-1), "\n"; // ✗Fatal error: Uncaught ValueError: str_repeat(): Argument #2 ($times) must be greater than or equal to 0Первый вызов напечатал ***. Во втором str_repeat получила отрицательное число и бросила ValueError. Блок catch его не поймал, и программа остановилась.
У курьера посылки бывают двух видов: исключения приходят из вашего кода и библиотек, а ошибки — из движка PHP. Обе идут вверх по одной лестнице, но дверь с табличкой Exception открывается только для исключений. Посылке-ошибке нужна дверь Error или Throwable.
Две ветви под Throwable
Всё, что PHP умеет бросать, реализует интерфейс Throwable. Под ним две ветви, и они не пересекаются.
Ветвь Exception
Исключения бросает ваш код и библиотеки, когда что-то пошло не так в работе программы:
Exception— общий класс всей ветви;InvalidArgumentException— функцию вызвали с неправильным аргументом; родитель этого класса —LogicException, «ошибка в логике вызова»;RuntimeException— сбой, который видно только во время работы: нет файла, кончилось место на диске.
Ветвь Error
Ошибки бросает движок, когда сам не может выполнить строку:
TypeError— аргумент не того типа; потомок этого класса,ArgumentCountError, — аргументов не хватает;ValueError— тип верный, а значение нет, как-1вstr_repeat;DivisionByZeroError— деление на ноль;Error— общий класс ветви, например при вызове несуществующей функции.
catch ловит свой класс и его потомков, а Error не потомок Exception. Поэтому catch (Exception $e) никогда не поймает Error. Чем Exception отличается от Error — классический вопрос на собеседовании, и ответ именно такой: это две разные ветви под Throwable.
Здесь картинка с дверями перестаёт работать. Настоящий курьер мог бы отдать посылку кому угодно, а catch проверяет класс, и к чужой ветви дверь не откроется.
Как поймать Error
Чтобы поймать ошибку движка, в catch называют её класс:
<?php
function stars(int $count): string
{
try {
return str_repeat('*', $count);
} catch (ValueError $e) {
return '';
}
}
echo '[', stars(-1), ']', "\n"; // []Теперь stars(-1) возвращает пустую строку. Но чаще ошибка движка значит, что неверен сам код. Тогда лучше исправить код, а не прятать ошибку в catch: например, проверить $count перед вызовом.
Throwable на внешней границе
catch (Throwable $e) ловит обе ветви. Его ставят один раз, на внешней границе программы — вокруг всего, что она делает. Там он показывает понятное сообщение вместо пустой страницы, а подробности можно записать в журнал; в примере их печатает echo:
<?php
function report(): void
{
echo 'counting', "\n"; // counting
echo intdiv(1, 0), "\n";
}
try {
report();
} catch (Throwable $e) {
echo 'Sorry, something broke', "\n"; // Sorry, something broke
echo get_class($e), "\n"; // DivisionByZeroError
}Функция get_class возвращает имя класса объекта: по нему видно, что именно случилось. Глубоко внутри программы catch (Throwable $e) не ставят: он поймает и ошибку в коде, которую нужно исправлять, а не глушить.
Частая ошибка
Чтобы получить свой класс исключения, пробуют реализовать интерфейс Throwable напрямую:
<?php
class PaymentFailed implements Throwable {} // ✗Fatal error: Class PaymentFailed cannot implement interface Throwable, extend Exception or Error insteadPHP не даёт реализовать Throwable своему классу. Бросаемым класс становится только через наследование, как подсказывает само сообщение: class PaymentFailed extends Exception {}.
Главное
Throwable— интерфейс, под ним две отдельные ветви:Exceptionдля сбоев программы иErrorдля ошибок движка.catch (Exception $e)не ловитTypeError,ValueErrorиDivisionByZeroError: они в ветвиError.- Ошибку движка ловят по её классу, а чаще исправляют код, который её вызвал.
catch (Throwable $e)ловит всё и стоит один раз, на внешней границе программы.