Обработка ошибок и исключений в PHP: рабочие приёмы
Warning вместо остановки, Throwable вместо Exception, пустой catch, который прячет причину падения. Разбираем, как обрабатывать ошибки в PHP 8.x так, чтобы находить их в логах, а не гадать.

Классическая ситуация: код падает в проде, в логах ничего внятного, а разработчик оборачивает подозрительный участок в try { ... } catch (Exception $e) {} и радуется, что «не падает». На деле он только что спрятал причину сбоя. С PHP 7 модель ошибок изменилась: часть того, что раньше было фатальными ошибками, стала исключениями, а ловить их через Exception недостаточно. Разберём, как ловить правильно, чем Error отличается от Exception, где пустой catch вредит и как настроить логирование, чтобы причина падения оказалась в файле, а не в пустоте. Актуально для PHP 8.0–8.3.
Две ветки: Error и Exception
С PHP 7 у всех бросаемых объектов появился общий интерфейс Throwable. От него наследуются две ветки:
- Exception — прикладные ошибки, которые вы бросаете и ловите сами: не прошла валидация, недоступен внешний сервис, запись не найдена.
- Error — ошибки уровня движка:
TypeError,DivisionByZeroError,ArgumentCountError, вызов несуществующего метода. Раньше это были фатальные ошибки, теперь их можно поймать.
Ключевой момент: Error не наследуется от Exception. Поэтому привычный catch (Exception $e) не поймает TypeError — процесс всё равно упадёт с фатальной ошибкой, только вы будете думать, что защитились.
<?php
// PHP 8.x
declare(strict_types=1);
function divide(int $a, int $b): float {
return $a / $b; // при $b === 0 бросит DivisionByZeroError
}
try {
echo divide(10, 0);
} catch (Exception $e) {
// сюда НЕ попадём: DivisionByZeroError наследуется от Error, а не Exception
echo 'поймали: ' . $e->getMessage();
}
// результат: Fatal error: Uncaught DivisionByZeroError
Чтобы поймать обе ветки сразу, ловите Throwable:
<?php
try {
echo divide(10, 0);
} catch (DivisionByZeroError $e) {
// конкретный тип — предпочтительнее общего
echo 'деление на ноль: ' . $e->getMessage();
} catch (Throwable $e) {
// всё остальное, что может прилететь
echo 'непредвиденная ошибка: ' . $e->getMessage();
}
Что ловить: Throwable, Exception или конкретный класс
Правило простое: ловите настолько узко, насколько можете обработать. Если вы умеете обработать только «сервис недоступен» — ловите конкретный класс исключения этого сервиса, а не всё подряд. Общий catch (Throwable $e) уместен на верхнем уровне приложения как последний рубеж, чтобы залогировать и показать пользователю аккуратную 500-ю, а не белый экран.
| Что поймает | Exception | Error | Когда использовать |
|---|---|---|---|
catch (RuntimeException $e) | только этот тип и наследников | нет | когда знаете, как обработать именно этот случай |
catch (Exception $e) | да | нет | прикладные ошибки, не баги движка |
catch (Throwable $e) | да | да | верхний рубеж: залогировать и не упасть молча |
Ошибки, которые не исключения
Не всё в PHP бросается. Warning, Notice, Deprecated — это не исключения, их catch не поймает. Обращение к несуществующему ключу массива, file_get_contents на несуществующий файл, деление строки — всё это генерирует warning и продолжает выполнение с подозрительным значением вроде null или false.
Есть два способа сделать так, чтобы такие ошибки не проходили мимо.
Превратить ошибки в исключения через set_error_handler
Регистрируете обработчик, который бросает ErrorException. После этого warning можно ловить обычным try/catch.
<?php
// PHP 8.x — превращаем warning/notice в исключения
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
// уважаем @-подавление и текущий error_reporting
if (!(error_reporting() & $severity)) {
return false;
}
throw new ErrorException($message, 0, $severity, $file, $line);
});
try {
$content = file_get_contents('/no/such/file');
} catch (ErrorException $e) {
// теперь warning стал ловимым
error_log('Не удалось прочитать файл: ' . $e->getMessage());
$content = '';
}
Пустой блок catch без логирования — это не обработка ошибки, а её сокрытие. Через месяц вы будете искать причину падения в коде, который «работает нормально», потому что молча проглотил исключение.
Глобальные обработчики: последний рубеж
Три функции ловят то, что не поймал try/catch, и не дают приложению упасть молча:
set_exception_handler— вызывается для непойманного исключения перед завершением скрипта. Здесь логируете и отдаёте пользователю понятную страницу.register_shutdown_functionвместе сerror_get_last— единственный способ поймать фатальные ошибки вроде исчерпания памяти, которые не превращаются в исключения.set_error_handler— для warning/notice, как выше.
<?php
set_exception_handler(function (Throwable $e): void {
error_log(sprintf(
"Uncaught %s: %s in %s:%d",
get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()
));
http_response_code(500);
echo 'Внутренняя ошибка. Попробуйте позже.';
});
register_shutdown_function(function (): void {
$error = error_get_last();
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR], true)) {
error_log('Fatal: ' . $error['message'] . ' in ' . $error['file'] . ':' . $error['line']);
}
});
finally и очистка ресурсов
Блок finally выполняется всегда — и после нормального завершения, и после исключения, и даже после return внутри try. Это правильное место для освобождения ресурсов: закрыть соединение, снять блокировку, удалить временный файл.
<?php
$lock = fopen('/tmp/job.lock', 'w');
flock($lock, LOCK_EX);
try {
// работа, которая может бросить исключение
processQueue();
} finally {
// блокировка снимется в любом случае
flock($lock, LOCK_UN);
fclose($lock);
}
Не используйте finally для того, что должно происходить только при успехе — для этого есть конец блока try. И осторожно с return внутри finally: он перекроет и значение из try, и брошенное исключение. Это источник тихих багов.
Свои классы исключений
Для прикладной логики создавайте собственные типы. Тогда вызывающий код ловит именно нужную ситуацию, а не разбирает строку сообщения.
<?php
class PaymentException extends RuntimeException {}
class InsufficientFundsException extends PaymentException {}
class GatewayUnavailableException extends PaymentException {}
try {
$gateway->charge($order);
} catch (InsufficientFundsException $e) {
// показать пользователю: не хватает средств
} catch (GatewayUnavailableException $e) {
// повторить позже, поставить в очередь
} catch (PaymentException $e) {
// любая другая проблема оплаты
}
Наследование от базового PaymentException даёт гибкость: можно ловить конкретный подтип или всю группу разом. Заводите иерархию под домен, а не десяток несвязанных классов.
Типичные ошибки
- Пустой
catch. Исключение проглочено, причина потеряна. Минимум —error_log. catch (Exception $e)там, где может прилететьError.TypeErrorпройдёт мимо и уронит процесс.- Показ
$e->getMessage()пользователю. В сообщении может быть путь к файлу, имя таблицы, фрагмент SQL. Логируйте детали, пользователю — общий текст. - Подавление через
@вместо обработки. Ошибка исчезает из виду, но проблема остаётся. - Потеря контекста при повторном бросании. Если оборачиваете исключение, передавайте оригинал третьим аргументом:
throw new AppException('оплата не прошла', 0, $e). ТогдаgetPrevious()вернёт исходную причину.
Что настроить дальше
В проде выключите вывод ошибок на экран и включите запись в файл: директивы display_errors = Off и log_errors = On в php.ini. Точные имена директив и их поведение сверяйте в официальной документации по разделу настроек обработки ошибок для вашей версии. Дальше подключите структурированное логирование — например, Monolog: он пишет исключения со стектрейсом и контекстом в файл, Sentry или другой сборщик, а не оставляет вас с одной строкой в error_log. И договоритесь в команде, какие исключения ловятся на месте, а какие идут до верхнего рубежа — это решает половину споров на ревью.
Контент-конвейер: Из одной идеи — текст, изображение, озвучка и ролик. Один заказ вместо четырёх.
Спросить за 60 ₽Источники: PHP Manual — Exceptions, PHP Manual — Predefined Exceptions (Throwable, Error, Exception)
Частые вопросы
Чем catch (Throwable) отличается от catch (Exception) в PHP 8?
У меня PHP 7.4, а не 8.x — приёмы сработают?
Почему мой пустой catch не помогает найти причину падения?
Как поймать фатальную ошибку вроде исчерпания памяти?
Стоит ли показывать пользователю текст исключения?
Обработка ошибок через set_error_handler замедляет приложение?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.