Перейти к основному содержимому

Обработка исключений в прикладном коде PHP

Разработчику Архитектору

Статья про как писать код с исключениями. Дерево встроенных классов Throwable — в материале Иерархия исключений в PHP.

Теория — обработка исключений

С PHP 7 любой перехватываемый сбой — объект ThrowableException для ожидаемых сбоев библиотек и домена, 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 — когда бросать

Бросать исключение уместно, когда:

  1. Метод не может выполнить контракт при текущих данных (неверный email, пустой файл).
  2. Вызывающий код должен решить, что делать (повторить, показать форму, откатить транзакцию).
  3. Ошибка не является нормальным ветвлением (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 как основа веб-интеграций.