В 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.