Перейти к содержимому
Теория · Ошибки и исключения · урок 4 из 8

Иерархия Throwable

Блок catch (Exception $e) ловит Exception и его дочерние классы. Кажется, что этого хватит на любой сбой. Давайте проверим на ошибке, которую бросает сам PHP:

PHP
<?php

function stars(int $count): string
{
    try {
        return str_repeat('*', $count);
    } catch (Exception $e) {
        return '';
    }
}

echo stars(3), "\n"; // ***
echo stars(-1), "\n"; // ✗
TEXT
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
<?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
<?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
<?php

class PaymentFailed implements Throwable {} // ✗
TEXT
Fatal error: Class PaymentFailed cannot implement interface Throwable, extend Exception or Error instead

PHP не даёт реализовать Throwable своему классу. Бросаемым класс становится только через наследование, как подсказывает само сообщение: class PaymentFailed extends Exception {}.

Главное

  • Throwable — интерфейс, под ним две отдельные ветви: Exception для сбоев программы и Error для ошибок движка.
  • catch (Exception $e) не ловит TypeError, ValueError и DivisionByZeroError: они в ветви Error.
  • Ошибку движка ловят по её классу, а чаще исправляют код, который её вызвал.
  • catch (Throwable $e) ловит всё и стоит один раз, на внешней границе программы.