Обработка ошибок и исключений в PHP: что нужно знать до начала
Разница между ошибками и исключениями, почему @ и die() вредят, и как настроить перехват так, чтобы вы видели проблему в логе, а пользователь — вежливую страницу вместо белого экрана.

К концу этого материала у вас будет рабочая связка из трёх частей: правильные настройки отображения ошибок для разработки и для боевого сервера, один общий обработчик, который ловит всё разом, и понимание, чем Error отличается от Exception. На настройку уйдёт минут двадцать. Всё, что ниже, проверено на PHP 8.1–8.3; для PHP 7.x есть отдельная оговорка в тексте.
Что должно быть до начала
- PHP версии 8.0 или выше. Проверьте командой
php -v. На 7.4 всё работает похоже, но часть ошибок движка стала исключениями именно в 7.x, а типизированные свойства — в 7.4, так что примеры ориентированы на 8.x. - Доступ к
php.iniили возможность задавать директивы через.htaccess/ini_set(). Без права менять настройки вы не сможете выключить вывод ошибок на проде. - Каталог для логов, куда у процесса PHP есть право на запись. Обычно это
/var/log/php/или каталог внутри проекта.
Три разных зверя: ошибки, исключения, фатальные ошибки
Главная путаница новичка в том, что в PHP исторически сосуществуют два механизма сообщений о проблемах, и они ведут себя по-разному.
- Ошибки уровня движка —
E_WARNING,E_NOTICE,E_DEPRECATED. Например, обращение к несуществующему ключу массива или к необъявленной переменной. Скрипт при них обычно продолжает работу, а сообщение либо выводится, либо пишется в лог — в зависимости от настроек. - Исключения — объекты классов, наследующих
Exception. Их бросают черезthrowи ловят черезtry/catch. Это управляемый механизм: вы сами решаете, где и как реагировать. - Фатальные ошибки движка в PHP 7+ превратились в объекты класса
Error(например,TypeError,DivisionByZeroError). Их тоже можно ловить вtry/catch, но по классуError, а неException.
Общий предок для обоих типов исключаемых объектов — интерфейс Throwable. Именно его удобно ловить, когда нужно поймать «вообще всё, что бросается».
Почему нельзя ловить всё подряд через catch (Exception)
Частая ошибка: разработчик пишет catch (Exception $e) и удивляется, что TypeError пролетает мимо. Причина в иерархии: Error не наследует Exception. Оба реализуют Throwable, но это параллельные ветки.
<?php
// PHP 8.x
try {
$result = intdiv(10, 0); // бросит DivisionByZeroError
} catch (Exception $e) {
// сюда НЕ попадём: DivisionByZeroError наследует Error, а не Exception
echo 'Поймали Exception';
} catch (Throwable $e) {
// а вот сюда попадём
echo 'Поймали через Throwable: ' . $e->getMessage();
}
Вывод: Поймали через Throwable: Division by zero. Запомните правило — ловите Throwable, если хотите гарантированно поймать и ошибки движка, и ваши исключения. Ловите конкретный класс, когда реагируете на конкретную ситуацию.
Настройки: разные для разработки и для прода
Самая опасная привычка — оставить на боевом сервере вывод ошибок в браузер. Пользователь увидит путь к файлам и структуру кода, а иногда и параметры подключения к базе. Настройки должны быть зеркальными.
| Директива | Разработка | Прод | Что делает |
|---|---|---|---|
display_errors | On | Off | Печатать ли ошибки в вывод (браузер) |
log_errors | On | On | Писать ли ошибки в лог-файл |
error_log | путь к файлу | путь к файлу | Куда писать лог |
error_reporting | E_ALL | E_ALL | Какие уровни ошибок учитывать |
На проде вы отключаете только показ в браузер, но не логирование. Иначе ошибки исчезнут бесследно. Пример настройки прямо в коде на входе приложения (если нет доступа к php.ini):
<?php
// index.php — единая точка входа. PHP 8.x
error_reporting(E_ALL);
ini_set('display_errors', '0'); // на проде: не показываем пользователю
ini_set('log_errors', '1');
ini_set('error_log', __DIR__ . '/logs/php-error.log');
Проверка: вызовите заведомо ошибочный код (обратитесь к $undefined_variable) и убедитесь, что в браузере ничего не появилось, а в logs/php-error.log добавилась строка с текстом и номером строки.
Оператор подавления ошибок@перед выражением выглядит как быстрое решение, а на деле прячет проблему целиком: ошибка не попадёт ни в браузер, ни в лог. Отлаживать код, засеянный собаками, — отдельный вид боли. Не используйте@, а обрабатывайте ситуацию явно.
try/catch: как писать, чтобы было полезно
Три правила, которые отделяют рабочий перехват от бесполезного.
- Ловите то, что можете обработать. Если в
catchвы просто пишетеecho 'ошибка'и продолжаете, будто ничего не случилось, — вы прячете баг, а не решаете его. - Не глотайте исключение молча. Пустой блок
catch— источник многочасового поиска «почему ничего не сохранилось». Как минимум залогируйте. - Используйте
finallyдля освобождения ресурсов. Код вfinallyвыполнится независимо от того, было исключение или нет.
<?php
// PHP 8.x — чтение файла с корректной обработкой
$handle = null;
try {
$handle = fopen('/path/to/data.txt', 'r');
if ($handle === false) {
throw new RuntimeException('Не удалось открыть файл');
}
$content = fread($handle, 1024);
// ...работа с содержимым
} catch (RuntimeException $e) {
// логируем с контекстом, а не просто echo
error_log('Ошибка чтения: ' . $e->getMessage());
// решаем, что показать пользователю или как продолжить
} finally {
// закроем дескриптор в любом случае
if (is_resource($handle)) {
fclose($handle);
}
}
Несколько catch и объединение типов
С PHP 8.0 можно ловить несколько классов в одном блоке через вертикальную черту, а порядок блоков важен: сначала более конкретные классы, потом общие.
<?php
// PHP 8.x
try {
// код, который может бросить разные исключения
} catch (InvalidArgumentException | LengthException $e) {
// общая реакция на две ошибки валидации
} catch (Throwable $e) {
// всё остальное как страховка
}
Глобальные обработчики: последняя линия обороны
Часть проблем невозможно обернуть в try/catch — например, E_WARNING при делении или необработанное исключение, всплывшее до верха стека. Для этого PHP даёт три перехватчика, которые ставятся один раз на входе в приложение.
<?php
// bootstrap приложения. PHP 8.x
// 1. Превращаем ошибки движка в исключения, чтобы ловить единообразно
set_error_handler(function (int $severity, string $message, string $file, int $line) {
// уважаем текущий error_reporting: подавленные через @ не трогаем
if (!(error_reporting() & $severity)) {
return false;
}
throw new ErrorException($message, 0, $severity, $file, $line);
});
// 2. Ловим всё, что не поймали в try/catch
set_exception_handler(function (Throwable $e) {
error_log('Необработанное: ' . $e->getMessage() . ' в ' . $e->getFile() . ':' . $e->getLine());
http_response_code(500);
// пользователю — нейтральная страница без деталей
echo 'Внутренняя ошибка. Мы уже разбираемся.';
});
// 3. Ловим фатальные ошибки, которые убивают скрипт
register_shutdown_function(function () {
$error = error_get_last();
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
error_log('Фатальная: ' . $error['message'] . ' в ' . $error['file'] . ':' . $error['line']);
http_response_code(500);
}
});
Проверка: бросьте необработанное исключение в любом контроллере. В браузере должна появиться ваша нейтральная страница с кодом 500, а в логе — полная строка с файлом и номером строки.
Если не получилось
- Белый экран, в логе пусто. Скорее всего сработала ошибка парсинга (
E_PARSE) до того, как выполнился ваш bootstrap, либо у процесса нет прав на запись в файл лога. Проверьте синтаксис командойphp -l index.phpи права на каталог логов. - «Cannot modify header information — headers already sent». Вы попытались вызвать
http_response_code()после того, как что-то уже вывелось в браузер (лишний пробел до<?php, случайныйecho). Уберите любой вывод до отправки заголовков. - Ошибки движка по-прежнему видны на проде. Значит
display_errorsгде-то переопределяется — проверьте, не задаётся ли директива в.htaccess, в настройках FastCGI или в самом коде после вашего блока. - set_error_handler не ловит фатальные ошибки. Так и должно быть:
E_ERROR,E_PARSEи подобные обработчик ошибок перехватить не может. Для них естьregister_shutdown_function. - catch (Exception) пропускает TypeError. Меняйте на
catch (Throwable)—Errorи его наследники не относятся кException.
Что делать дальше
Когда базовый перехват работает, следующий шаг — подключить полноценное логирование через библиотеку Monolog вместо error_log(): она умеет уровни, ротацию и отправку в разные каналы. Дальше стоит посмотреть в сторону сервиса мониторинга ошибок (Sentry и аналоги), который соберёт трассировки и уведомит вас раньше, чем напишет пользователь. Отдельная тема — собственные классы исключений под конкретные ситуации домена; здесь мы её не разворачивали, но именно с них начинается аккуратная архитектура обработки ошибок.
Агент-продажник: Скрипты B2B-продаж, обработка возражений по BANT и SPIN.
Спросить за 20 ₽Источники: PHP Manual — Exceptions, PHP Manual — Errors and error handling
Частые вопросы
У меня PHP 7.4, примеры сработают?
Чем отличается Error от Exception?
Можно ли поймать ошибку парсинга (E_PARSE) в try/catch?
Что будет, если я оставлю display_errors включённым на проде?
Нужно ли оборачивать каждый вызов в try/catch?
Почему не стоит использовать оператор @ для подавления ошибок?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.