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

Полная форма состоит из трёх частей. Обязателен только try плюс хотя бы один 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 (вызов несуществующего метода, неверный тип аргумента, деление на ноль), выбрасывают Error, а не Exception, а 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 пробует их сверху вниз и использует первое совпадение, поэтому конкретный тип должен стоять перед своим родителем, иначе родитель перехватит всё первым. Когда несколько типов обрабатываются одинаково, объедините их в одном catch через |:

Все три класса наследуют Exception, поэтому один catch (Exception $e) тоже их поймал бы, но тогда код уже не смог бы ответить 404 на одно и 400 на остальные.

finally: код, который выполняется всегда

Блок finally выполняется после try и catch, как бы они ни завершились: обычно, с перехваченным исключением, с исключением, которое никто не поймал, или с return. Это место, чтобы освободить то, что захватил блок try: закрыть файл, снять блокировку, остановить таймер.

Для good.txt файл закрывается раньше, чем выводится saved, потому что finally выполняется до фактического возврата из функции. Для bad.txt внутри process() нет catch: finally закрывает файл, а потом исключение идёт дальше к catch вызывающего кода.

Избегайте return внутри finally: он заменяет значение, которое вернул блок 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; повторно выбрасывает то же исключение. Чаще нужно добавить контекст, сохранив исходную причину, и для этого служит третий аргумент конструктора, $previous:

Вызывающий код имеет дело с одним 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() регистрирует функцию, которая получает любое исключение, которое никто не поймал: это единое место, чтобы записать его в лог и вывести вежливую страницу:

Обработчик выполняется, а потом скрипт останавливается; продолжить работу после throw нельзя. Показывается ли собственный текст ошибки PHP на странице вообще, определяет display_errors, об этом в разделе о выводе ошибок.

Предупреждения это не исключения

try/catch видит только то, что выброшено. Многие старые функции PHP не выбрасывают исключений: они возвращают false или null и в лучшем случае печатают предупреждение. Типичный пример json_decode(), и обёртка в try ничего не меняет, пока вы не попросите функцию выбрасывать исключения:

Для функций без такого флага проверяйте возвращаемое значение (if ($handle === false)) или заранее проверяйте условие (file_exists(), isset()). set_error_handler(), который выбрасывает ErrorException, превращает каждое предупреждение в настоящее исключение, если вам нужно одно правило для всего.

Рецепт: проверка ввода формы через исключения

Практичный шаблон: функции проверки выбрасывают исключения, а страница перехватывает их и показывает сообщение рядом с формой. Запустите, затем введите в форму возраст и нажмите Send: тот же скрипт выполнится снова с заполненным $_POST.

Попробуйте пустое поле, twelve, 9 и 30. Каждое нарушенное правило выбрасывает своё сообщение, а один catch превращает их все в одну и ту же красную строку. На странице о формах этот приём развит до нескольких полей с ошибкой рядом с каждым.

Часто задаваемые вопросы

Как работает try catch в PHP?

PHP выполняет код внутри try { }. Если что-то в нём выбрасывает исключение, PHP пропускает остаток блока try и выполняет первый блок catch, тип которого подходит, а выброшенный объект попадает в переменную: catch (Exception $e) { echo $e->getMessage(); }. Если ничего не выброшено, блоки catch пропускаются.

Как перехватить все исключения и ошибки в PHP?

Перехватывайте Throwable: catch (Throwable $e). catch (Exception $e) ловит только исключения, но не ошибки движка вроде TypeError, ValueError или DivisionByZeroError, которые наследуют Error. И Exception, и Error реализуют Throwable.

Как перехватить несколько исключений в одном блоке catch в PHP?

Разделите типы вертикальной чертой: catch (InvalidArgumentException | RangeException $e). Можно также написать несколько блоков catch подряд; PHP использует первый подходящий, поэтому самые конкретные типы ставьте первыми.

Выполняется ли finally, если в try есть return?

Да. finally выполняется после try и catch независимо от того, вернул ли код значение, выбросил исключение или завершился обычно, в том числе когда в try есть return. Если finally сам возвращает значение, оно заменяет значение из try, поэтому избегайте return внутри finally.

Перехватывает ли try catch предупреждения в PHP?

Нет. Предупреждения и notice (например, чтение отсутствующего ключа массива) не являются исключениями, поэтому catch их никогда не видит. Проверяйте условие до вызова, используйте режим исключений функции там, где он есть (json_decode(..., flags: JSON_THROW_ON_ERROR)), или превращайте предупреждения в исключения через set_error_handler() и ErrorException.

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

НАЧАТЬ