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

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

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

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


Исключение и ошибка в PHP 7+

Любой сбой, который нужно обработать или залогировать, представлен объектом Throwable:

  • Exception — ожидаемые сбои бизнес-логики и библиотек (InvalidArgumentException, PDOException).
  • Error — ошибки уровня языка (TypeError, DivisionByZeroError), раньше обрывавшие скрипт.

Оба типа перехватываются одинаково. Для «поймать всё» используют catch (Throwable $e).


Базовая конструкция try / catch / finally

<?php
declare(strict_types=1);

function loadConfig(string $path): array
{
if (!is_readable($path)) {
throw new \RuntimeException("Config not readable: {$path}");
}

$json = file_get_contents($path);
$data = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
return $data;
}

try {
$config = loadConfig(__DIR__ . '/config.json');
} catch (\JsonException $e) {
fwrite(STDERR, "Invalid JSON: " . $e->getMessage() . PHP_EOL);
$config = [];
} catch (\RuntimeException $e) {
fwrite(STDERR, $e->getMessage() . PHP_EOL);
$config = [];
} finally {
// Выполняется всегда: и после успеха, и после catch, и перед exit в 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);
}

Не стоит бросать исключения для обычного потока («пользователь не найден» в API иногда возвращают как null или Option, а в веб-форме — как сообщение без stack trace).


Перехват Exception и Error

try {
processOrder($id);
} catch (\PDOException $e) {
// База недоступна — отдельная ветка
logError('db', $e);
showServiceUnavailable();
} catch (\TypeError $e) {
// Ошибка типов — чаще баг в коде
logError('bug', $e);
throw $e; // проброс после логирования
} catch (\Throwable $e) {
logError('general', $e);
showGenericError();
}

Проброс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);
}
}

Использование:

if ($email === '') {
throw new ValidationException(
['email' => 'Укажите email'],
);
}

Не наследуйте бизнес-исключения от 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

Режим 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() наружу
}

В CLI тот же Throwable пишут в STDERR и завершают процесс ненулевым кодом.


Связь с иерархией типов

При выборе класса исключения ориентируйтесь на семантику из справочника иерархии:

  • InvalidArgumentException — неверный аргумент функции.
  • DomainException — нарушение правила предметной области.
  • RuntimeException — сбой среды (файл, сеть), который нельзя предусмотреть при компиляции.

Что изучить дальше


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).