Ошибки в PHP: Fatal error, Warning, Notice и как их исправить

Ошибки в PHP: Fatal error, Warning, Notice и как их исправить Полезное

Fatal error в PHP — это ошибка, после которой интерпретатор останавливает выполнение скрипта: код после проблемной строки не выполняется, а пользователь получает оборванную страницу или ответ 500. Остальные сообщения — Warning, Notice, Deprecated — скрипт не останавливают: PHP сообщает о проблеме и идет дальше, часто уже с неверными данными.

Ниже — как читать Fatal error и чинить частые случаи, чем уровни ошибок отличаются от исключений Error/Exception и как настроить display_errors и log_errors для разработки и продакшена. Примеры и тексты сообщений получены прогоном в PHP 8.4.25 (CLI) 24.09.2026.

Мини-словарь: ошибка, исключение, Throwable

В PHP 8 «ошибками» называют два разных механизма:

  • Уровень ошибки (E_WARNING, E_NOTICE, E_DEPRECATED, E_ERROR и др.) — сообщение движка. try/catch его не ловит, нефатальные уровни перехватывает set_error_handler().
  • Исключение — объект, который бросается (throw) и ловится try/catch; все исключения реализуют интерфейс Throwable. Ветка Error — ошибки движка (несуществующая функция, неверный тип, intdiv() на ноль), ветка Exception — ошибки логики, которые бросает ваш код или библиотека.

В PHP 8 большинство фатальных ошибок выглядит как «Fatal error: Uncaught …» — это непойманный Error или Exception. Фатальные уровни без объекта (нехватка памяти, превышение времени, синтаксис) встречаются реже, о них ниже.

Как читать сообщение Fatal error

Минимальный пример: функцию вызвали, но не объявили и не подключили.

<?php
echo "start\n";
$total = calculate_total([10, 20]);
echo "never\n";
start

Fatal error: Uncaught Error: Call to undefined function calculate_total() in /app/f1.php:3
Stack trace:
#0 {main}
  thrown in /app/f1.php on line 3

Строку start PHP вывел, never — уже нет. Разбор сообщения:

  1. Uncaught Error — брошено и не поймано исключение класса Error. Класс подсказывает природу проблемы: TypeError — типы, DivisionByZeroError — деление на ноль.
  2. Call to undefined function calculate_total() — суть, ее и ищут в документации.
  3. /app/f1.php:3 — файл и строка.
  4. Stack trace — цепочка вызовов; {main} внизу — основной код файла, выше — функции, через которые прошли.

Исправление здесь — объявить функцию или подключить файл, где она описана (обычно через автозагрузку Composer):

<?php
function calculate_total(array $prices): int
{
    return array_sum($prices);
}

echo calculate_total([10, 20]), "\n"; // 30

В журнале ошибок у сообщения есть префикс PHP Fatal error:, на экране — просто Fatal error:; в логах ищите по обоим вариантам. В PHP 8.5 стек вызовов добавляется и к фатальным ошибкам без объекта (директива fatal_error_backtraces, по умолчанию On).

Частые Fatal error и как их исправить

Сообщения получены прогоном на PHP 8.4; в PHP 7 часть текстов выглядела иначе.

Сообщение (фрагмент) Причина Что сделать
Call to undefined function calculate_total() Функция не объявлена или файл не подключен Проверить имя, require/автозагрузку, namespace
Class "User" not found Класс не подключен, неверный namespace или путь в автозагрузке use, composer dump-autoload, соответствие PSR-4
Call to a member function getName() on null Переменная оказалась null, а не объектом Проверить, что вернул поиск; ?-> или явная проверка
Failed opening required 'vendor/autoload.php' Нет файла для require Выполнить composer install, строить путь от __DIR__
must be of type int, string given (TypeError) Аргумент не того типа Привести и проверить вход до вызова
Cannot redeclare function helper() Функция объявлена дважды, часто из-за двойного include require_once или автозагрузка
Allowed memory size of 67108864 bytes exhausted Скрипт превысил memory_limit Обрабатывать данные порциями (генераторы, построчное чтение)
Maximum execution time of 1 second exceeded Превышен max_execution_time (значение из set_time_limit(1)) Искать бесконечный цикл или выносить долгую задачу в очередь
Parse error: Unclosed '{' on line 3 Синтаксическая ошибка Закрыть скобку, проверить php -l file.php

Поднимать memory_limit имеет смысл, только если объем данных такой по задаче: если скрипт читает большой файл целиком, лимит лишь отодвинет падение. Файл с Parse error не выполняется совсем, поэтому ini_set('display_errors', '1') внутри него не поможет — включайте показ в php.ini или конфиге пула PHP-FPM, а файл проверяйте php -l.

Warning, Notice, Deprecated: скрипт работает дальше

Нефатальные уровни не останавливают выполнение, и в этом их опасность: код продолжает работать с пустыми или неверными данными.

<?php
$price = $_GET['price'];                 // ключа нет
echo "Цена: $price\n";
include 'config.local.php';              // файла нет
echo end(explode(',', 'a,b,c')), "\n";   // Notice
echo "5 apples" + 1, "\n";
echo "дошли до конца\n";
Warning: Undefined array key "price" in /app/w1.php on line 2
Цена: 

Warning: include(config.local.php): Failed to open stream: No such file or directory in /app/w1.php on line 4

Warning: include(): Failed opening 'config.local.php' for inclusion (include_path='.:/usr/local/lib/php') in /app/w1.php on line 4

Notice: Only variables should be passed by reference in /app/w1.php on line 5
c

Warning: A non-numeric value encountered in /app/w1.php on line 6
6
дошли до конца

Скрипт дошел до конца, но с пустой ценой и без конфига. Исправленный вариант:

<?php
$price = $_GET['price'] ?? null;
if ($price === null) {
    echo "Цена не передана\n";
}
$local = __DIR__ . '/config.local.php';
if (is_file($local)) {
    require $local;
}
$parts = explode(',', 'a,b,c');
echo end($parts), "\n";
Цена не передана
c

Что важно знать про уровни в PHP 8:

  • Обращение к неопределенной переменной или ключу массива в PHP 8 — Warning, а не Notice, как в PHP 7.
  • Часть бывших предупреждений стала исключениями: арифметика со строкой без числа ("abc" + 0) бросает TypeError: Unsupported operand types, обращение к неопределенной константе — Error: Undefined constant.
  • Deprecated — конструкция объявлена устаревшей и может быть удалена в будущих версиях. В PHP 8.4 так помечены неявно nullable-параметр (string $name = null вместо ?string $name = null), константа E_STRICT (ее сообщения еще в PHP 7 перевели в другие уровни) и trigger_error() с E_USER_ERROR — вместо него бросайте исключение.

Error и Exception: что ловить в try/catch

Частая ошибка — ловить только Exception, ожидая, что это «все ошибки». Неверный вариант:

<?php
try {
    echo intdiv(100, 0);
} catch (Exception $e) {
    echo "Exception: ", $e->getMessage(), "\n";
}
echo "after\n";
Fatal error: Uncaught DivisionByZeroError: Division by zero in /app/c1.php:3

Это первая строка вывода, дальше идет стек. DivisionByZeroError — потомок Error, а не Exception, поэтому catch его пропустил, и after не вывелся. Исправление и иерархия:

<?php
$cases = [
    fn() => throw new RuntimeException('Товар A-1 не найден'), // Exception
    fn() => intdiv(100, 0),                                     // Error
];

foreach ($cases as $case) {
    try {
        $case();
    } catch (Exception $e) {
        echo 'Exception ', $e::class, ': ', $e->getMessage(), "\n";
    } catch (Error $e) {
        echo 'Error ', $e::class, ': ', $e->getMessage(), "\n";
    }
}
echo "скрипт продолжает работу\n";
Exception RuntimeException: Товар A-1 не найден
Error DivisionByZeroError: Division by zero
скрипт продолжает работу
Ветка Кто бросает Примеры классов Как обращаться
Exception Ваш код и библиотеки RuntimeException, InvalidArgumentException, PDOException Ловить там, где понятно, что делать дальше
Error Движок PHP TypeError, ValueError, DivisionByZeroError, ArgumentCountError Обычно это баг: исправлять код, а не глушить
Throwable Общий интерфейс обеих веток — Ловить на верхнем уровне: залогировать и ответить 500

Ловите конкретный класс там, где знаете, как восстановиться, а Throwable — один раз на верхнем уровне, не в каждой функции: иначе баги скрываются. Проверку лучше делать до опасной операции: if ($count === 0) throw new InvalidArgumentException(...) понятнее, чем ловить DivisionByZeroError после.

Что не ловится try/catch

Три вида фатальных ошибок не являются объектами: нехватка памяти, превышение времени выполнения и Parse error в самом файле. try/catch их не перехватит. Оператор @ в PHP 8 тоже не подавляет фатальные ошибки. Последнее, что можно сделать, — функция завершения:

<?php
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', '/tmp/app-error.log');

register_shutdown_function(function (): void {
    $err = error_get_last();
    $fatal = E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR;
    if ($err !== null && ($err['type'] & $fatal)) {
        echo "Сервис временно недоступен\n";
    }
});

$rows = [];
while (true) {
    $rows[] = str_repeat('x', 1024 * 1024);
}

С memory_limit=32M пользователь увидит строку «Сервис временно недоступен», а в журнале появится запись:

[24-Sep-2026 11:10:05 UTC] PHP Fatal error:  Allowed memory size of 33554432 bytes exhausted (tried to allocate 1052672 bytes) in /app/c4.php on line 16

Функция завершения не продолжает скрипт: она лишь позволяет вернуть аккуратный ответ.

display_errors и log_errors: разработка и продакшен

Две директивы решают разные задачи: display_errors — показывать ли ошибку в ответе, log_errors — писать ли ее в журнал. error_reporting задает, о каких уровнях сообщать вообще.

Директива Разработка (php.ini-development) Продакшен (php.ini-production)
error_reporting E_ALL E_ALL & ~E_DEPRECATED
display_errors On Off
display_startup_errors On Off
log_errors On On
error_log не задан не задан: указать путь или syslog

Значения — из шаблонов PHP 8.4. Без error_log запись уходит в обработчик SAPI: в CLI — stderr, у PHP-FPM и Apache — их журналы. Многие команды и на продакшене оставляют E_ALL, чтобы видеть Deprecated до обновления версии.

На проде display_errors выключают: сообщение раскрывает пути, имена функций и фрагменты данных. Проверка на встроенном сервере PHP 8.4, скрипт падает на вызове несуществующей функции:

  • display_errors=1 — 200 OK с текстом Fatal error и путем к файлу в HTML.
  • display_errors=0, до ошибки ничего не выведено — 500 Internal Server Error с пустым телом.
  • display_errors=0, но часть страницы уже выведена — 200 OK с оборванной страницей: заголовки отправлены.

Отсюда практический вывод: код ответа при ошибке надо ставить явно, а не рассчитывать на поведение движка.

Шаблон обработки ошибок для веб-приложения

Файл src/bootstrap.php подключается первым строкой в public/index.php. Журнал лежит в var/log/, вне document root, так что при запуске php -S 0.0.0.0:8000 -t public его нельзя скачать по URL.

<?php
declare(strict_types=1);

$isDev = getenv('APP_ENV') === 'dev';

error_reporting(E_ALL);                        // сообщать обо всем
ini_set('display_errors', $isDev ? '1' : '0'); // показывать - только в разработке
ini_set('log_errors', '1');                    // писать в лог - всегда
ini_set('error_log', dirname(__DIR__) . '/var/log/php-error.log');

// Warning/Notice/Deprecated -> ErrorException, чтобы их не пропускать
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
    if (!(error_reporting() & $severity)) {
        return false;
    }
    throw new ErrorException($message, 0, $severity, $file, $line);
});

// Последний рубеж для непойманных Exception и Error
set_exception_handler(function (Throwable $e) use ($isDev): void {
    error_log(sprintf('%s: %s in %s:%d', $e::class, $e->getMessage(), $e->getFile(), $e->getLine()));
    if (!headers_sent()) {
        http_response_code(500);
    }
    echo $isDev ? htmlspecialchars((string) $e) : 'Внутренняя ошибка сервера. Попробуйте позже.';
});
<?php
require dirname(__DIR__) . '/src/bootstrap.php';

$prices = ['A-1' => 1200];
$sku = $_GET['sku'] ?? 'B-2';
echo "Цена: " . $prices[$sku] . "\n";

Проверено запросами: ?sku=A-1 возвращает Цена: 1200, ?sku=B-2 — код 500 и «Внутренняя ошибка сервера», а в var/log/php-error.log появляется ErrorException: Undefined array key "B-2" in /app/public/index.php:6. Режим по умолчанию — продакшен; стек в ответе виден только при явном APP_ENV=dev. Текст исключения в dev-режиме экранируется htmlspecialchars: в сообщение попадает ключ из запроса, и без экранирования ?sku=<script>... исполнился бы в браузере.

Границы шаблона: фатальные уровни он не перехватывает (для них — register_shutdown_function) и не заменяет php.ini: Parse error в самом bootstrap случится раньше ini_set. Журнал нужно ротировать, пароли и персональные данные в него не писать.

Если не получилось: диагностика по симптому

  • Белый экран или 500 без текста. Фатальная ошибка при display_errors=Off, причина — в журнале PHP-FPM или веб-сервера. Если журнал пуст, проверьте, что каталог из error_log существует и доступен на запись пользователю PHP. Показ ошибок включайте только на локальной копии.
  • После обновления PHP посыпались Deprecated. Это не поломка, а список того, что перестанет работать в будущих версиях; исправляйте по одному, начиная с самых частых.

Выводы

  • Fatal error останавливает скрипт; Warning, Notice и Deprecated — нет, но приводят к работе с неверными данными.
  • В PHP 8 большинство «Fatal error: Uncaught …» — непойманные исключения Error или Exception; они ловятся try/catch, а Throwable покрывает обе ветки.
  • Нехватку памяти, превышение времени и Parse error try/catch не ловит: их видно только в журнале и через register_shutdown_function.
  • На продакшене display_errors=Off и log_errors=On, код 500 ставить явно; в разработке показывать все, включая E_DEPRECATED.

Где применяется / связь с практикой

Освойте тему на практике

Найти причину по сообщению и стеку, исправить код и настроить окружение так, чтобы следующая ошибка попала в журнал, а не в браузер, — повседневная работа бэкенд-разработчика. Системно PHP 8, обработку исключений, отладку и логирование в веб-приложениях разбирают на курсе «PHP-разработчик. Продвинутый уровень». Отдельные темы можно посмотреть на открытых уроках Otus.

FAQ

Чем Fatal error отличается от Parse error?
Parse error возникает при разборе файла, до выполнения, поэтому не выполняется ни одна его строка. Fatal error возникает во время выполнения, и код до проблемной строки успевает отработать.

Можно ли просто выключить вывод ошибок через error_reporting(0)?
Можно, но тогда ошибки не попадут и в журнал, а скрипт продолжит работать с неверными данными. На продакшене выключают показ (display_errors), а не сообщение об ошибках.

Можно ли поймать Parse error из подключаемого файла?
Да: синтаксическая ошибка в файле из include/require или в eval() бросается как исключение ParseError и ловится catch (ParseError $e). Не ловится только Parse error в файле, который запущен сам.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)