Обработка исключений в прикладном коде PHP
Статья про как писать код с исключениями. Дерево встроенных классов Throwable — в материале Иерархия исключений в PHP.
С PHP 7 любой перехватываемый сбой — объект Throwable — Exception для ожидаемых сбоев библиотек и домена, Error для бывших "фатальных" ошибок языка (TypeError, деление на ноль). Структурный блок try / catch / finally соответствует общей модели раскрутки стека. На границе веб-приложения уместен один catch (Throwable) с логированием и HTTP-ответом без утечки стека наружу.
Сначала прочитайте ошибка vs исключение и общую теорию. В PHP Error — фатальные сбои языка, Exception — перехватываемые отклонения.
Исключение и ошибка в PHP 7+
Любой сбой, который нужно обработать или залогировать, представлен объектом Throwable:
Exception— ожидаемые сбои бизнес-логики и библиотек (InvalidArgumentException,PDOException).Error— ошибки уровня языка (TypeError,DivisionByZeroError), раньше обрывавшие скрипт.
Оба типа перехватываются одинаково. Для "поймать всё" используют catch (Throwable $e).
На практике полезно разделять уровни:
- На границе приложения (front controller, entrypoint CLI) допустим общий
catch (Throwable $e). - Внутри доменного кода лучше ловить конкретные типы и принимать конкретные решения.
- Для инфраструктуры (БД, сеть, файловая система) стоит явно логировать контекст операции.
Базовая конструкция try / catch / finally
Код ITЗагрузка примера кода…
Разбор:
loadConfig(string $path): arrayзадает четкий контракт: на вход путь к конфигу, на выходе ассоциативные данные.is_readable($path)проверяет доступность файла заранее; при проблеме выбрасываетсяRuntimeExceptionс контекстом пути.json_decode(..., flags: JSON_THROW_ON_ERROR)переводит ошибки парсинга JSON вJsonException, а не в "тихие"null.- В
tryвыполняется основной сценарий, а отдельныеcatch-ветки обрабатывают разные типы сбоев независимо. finallyпредназначен для гарантированной очистки ресурсов и выполняется при любом исходе блокаtry/catch.
Порядок catch — от конкретного к общему. Сначала JsonException, потом RuntimeException, в конце при необходимости Throwable.
finally не подавляет исключение: если в catch снова выбросить ошибку или не обработать её, поведение сохраняется. finally удобен для закрытия файлов и транзакций (см. PDO).
throw — когда бросать
Бросать исключение уместно, когда:
- Метод не может выполнить контракт при текущих данных (неверный email, пустой файл).
- Вызывающий код должен решить, что делать (повторить, показать форму, откатить транзакцию).
- Ошибка не является нормальным ветвлением (
if (!$user) throw ...вместо тысячи вложенныхif).
function withdraw(Account $account, float $sum): void
{
if ($sum <= 0) {
throw new \InvalidArgumentException('Sum must be positive');
}
if ($account->balance < $sum) {
throw new \DomainException('Insufficient funds');
}
$account->debit($sum);
}
Разбор:
- Функция защищает доменные инварианты до изменения состояния счета.
InvalidArgumentExceptionвыбрасывается при технически некорректном аргументе ($sum <= 0).DomainExceptionотражает бизнес-ограничение ("недостаточно средств"), а не ошибку синтаксиса данных.- Только после прохождения проверок вызывается
$account->debit($sum), то есть изменение баланса происходит в валидном состоянии. - Такой порядок делает код безопаснее и упрощает обработку ошибок на уровне сервиса/контроллера.
Не стоит бросать исключения для обычного потока ("пользователь не найден" в API иногда возвращают как null или Option, а в веб-форме — как сообщение без stack trace).
Рабочее правило — если вызывающий код может разумно продолжить выполнение без аварийного пути, возвращаем значение (null, DTO результата). Если продолжать безопасно нельзя, выбрасываем исключение.
Перехват Exception и Error
Код ITЗагрузка примера кода…
Разбор:
- Первый
catch (\PDOException $e)выделяет инфраструктурную ошибку БД в отдельный канал обработки. catch (\TypeError $e)трактуется как возможный баг кода; после логирования исключение пробрасывается дальше черезthrow $e.catch (\Throwable $e)служит финальной защитной сеткой для любых непредусмотренных ошибок.- Разделение по типам исключений позволяет возвращать корректные реакции:
503для инфраструктуры,500для неизвестных сбоев. - Последовательность
catchважна: от более специфичных к более общемуThrowable.
Проброс — throw $e или throw new WrapperException('...', 0, $e). Второй аргумент 0 — код; третий — previous exception для цепочки в логах.
Пользовательские исключения
Наследуйте от Exception или от подходящего встроенного класса:
namespace App\Exception;
class ValidationException extends \InvalidArgumentException
{
/** @param array<string, string> $errors */
public function __construct(
public readonly array $errors,
string $message = 'Validation failed',
) {
parent::__construct($message);
}
}
Разбор:
ValidationExceptionнаследуется отInvalidArgumentException, поэтому семантически остается ошибкой входных данных.public readonly array $errorsхранит структурированные причины валидации, пригодные для API-ответа.- Конструктор принимает человекочитаемое сообщение и массив полей с ошибками одновременно.
- Такой класс стандартизирует передачу ошибок между слоями приложения и снижает дублирование формата.
Использование:
if ($email === '') {
throw new ValidationException(
['email' => 'Укажите email'],
);
}
Разбор:
- Условие проверяет пустой email через строгое сравнение
===, избегая неявных преобразований. - При нарушении правила сразу выбрасывается специализированное исключение с деталями по полю
email. - Массив ошибок позволяет фронтенду точно подсветить проблемное поле без дополнительного парсинга строки.
- Такой паттерн легко масштабируется на множественные ошибки формы и централизованную обработку
422.
Не наследуйте бизнес-исключения от Error — это тип для сбоев движка PHP.
PDO и json с исключениями
$pdo = new \PDO($dsn, $user, $pass, [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
]);
// Любая ошибка SQL → PDOException
$data = json_decode($input, true, flags: JSON_THROW_ON_ERROR);
// Ошибка формата → JsonException
Разбор:
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTIONпереводит SQL-сбои в исключения, что делает поток ошибок единообразным.- Без этого режима код должен вручную проверять результат почти каждого вызова PDO, что повышает риск пропусков.
json_decode(..., JSON_THROW_ON_ERROR)аналогично делает ошибки JSON явными и перехватываемыми.- В итоге и SQL, и JSON работают в одном стиле
try/catch, упрощая архитектуру error-handling.
Режим ERRMODE_EXCEPTION предпочтительнее ручной проверки после каждого вызова. Подробнее: PDO.
Антипаттерны
| Плохо | Лучше |
|---|---|
Пустой catch (Exception $e) {} | Логировать и/или пробрасывать |
die($e->getMessage()) в продакшене | Страница ошибки без деталей, детали в лог |
@file_get_contents(...) | try/catch или проверка + исключение |
Исключение для каждого if (!$row) | Явный возврат null / Result-объект |
| Показ stack trace пользователю | Только в dev (display_errors=On) |
Логирование и HTTP-ответ
Минимальный шаблон границы приложения:
try {
$response = $router->dispatch($_SERVER['REQUEST_URI'], $_SERVER['REQUEST_METHOD']);
echo $response;
} catch (ValidationException $e) {
http_response_code(422);
echo json_encode(['errors' => $e->errors], JSON_THROW_ON_ERROR);
} catch (\Throwable $e) {
error_log($e->__toString());
http_response_code(500);
echo 'Внутренняя ошибка'; // без $e->getMessage() наружу
}
Разбор:
- Это пример "границы приложения", где все исключения конвертируются в понятные HTTP-ответы.
ValidationExceptionмаппится в422и возвращает структурированный JSON с деталями ошибок.- Общий
catch (\Throwable $e)логирует полный стек черезerror_log(...), но наружу отдает нейтральное сообщение. - Такой шаблон предотвращает утечку внутренних деталей системы пользователю и сохраняет диагностическую информацию в логах.
- Разделение пользовательских и системных ошибок делает API предсказуемым для клиентов.
В CLI тот же Throwable пишут в STDERR и завершают процесс ненулевым кодом.
Связь с иерархией типов
При выборе класса исключения ориентируйтесь на семантику из справочника иерархии:
InvalidArgumentException— неверный аргумент функции.DomainException— нарушение правила предметной области.RuntimeException— сбой среды (файл, сеть), который нельзя предусмотреть при компиляции.
Что изучить дальше
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.