Перейти к содержанию
Educora
Средний17 мин8 / 10

Исключения и обработка ошибок

Перехватывай ошибки с помощью try/catch/finally, разберись в иерархии Exception и Error, создай собственный класс исключения и правильно настрой вывод ошибок для разработки и продакшена.

Проверь себя
В этом уроке ты узнаешь
  • Выбрасывать и перехватывать исключения с помощью throw, try, catch и finally
  • Объяснять иерархию Throwable, Exception и Error и писать собственное исключение
  • Правильно настраивать вывод ошибок для разработки и продакшена

Платёжная система не отвечает, пользователь прислал сломанный JSON, база данных недоступна — на реальном сайте такое случается каждый день. Хорошая программа не показывает «белый экран»: она перехватывает проблему, выдаёт пользователю понятное сообщение, а подробности записывает в журнал. Главный инструмент для этого в PHP — исключения (exceptions).

try, catch и finally

Обнаружив проблему, код выбрасывает исключение с помощью throw: функция сразу останавливается, а исключение поднимается по цепочке вызовов, пока не найдёт блок catch, который его перехватит. Опасный код помещают в блок try, catch перехватывает исключение определённого типа, а finally выполняется в любом случае — было исключение или нет.

PHP
<?php

function divide(int $a, int $b): float
{
    if ($b === 0) {
        throw new InvalidArgumentException("Cannot divide by zero");
    }
    return $a / $b;
}

foreach ([4, 0] as $b) {
    try {
        $result = divide(12, $b);
        echo "12 / $b = $result\n";
    } catch (InvalidArgumentException $e) {
        echo "Error: ", $e->getMessage(), "\n";
    } finally {
        echo "-- done with $b\n";
    }
}
Ожидаемый результат
12 / 4 = 3
-- done with 4
Error: Cannot divide by zero
-- done with 0

При $b = 0 строка с echo так и не выполнилась: после throw управление сразу перешло в catch. А finally сработал на обоих проходах цикла. Полезные методы объекта исключения: getMessage() — сообщение, getCode() — код, getFile() и getLine() — место ошибки, getPrevious() — предыдущее исключение.

Exception и Error

Начиная с PHP 7 общий родитель всего, что можно выбросить, — интерфейс Throwable. От него отходят две ветви: **Exception — проблемы в логике самой программы (неверный аргумент, файл не найден, не удалось подключиться к базе) и Error** — внутренние ошибки PHP (неверный тип, деление на ноль, вызов несуществующего метода).

ВетвьЧасто встречающиеся классыКогда
ExceptionInvalidArgumentException, RuntimeException, JsonException, PDOExceptionожидаемые проблемы: их можно перехватить и обработать
ErrorTypeError, ValueError, DivisionByZeroError, ArgumentCountError, UnhandledMatchErrorобычно ошибка в коде: её нужно исправить
PHP
<?php

$tests = [
    fn() => intdiv(10, 0),
    fn() => json_decode("{bad json", flags: JSON_THROW_ON_ERROR),
    fn() => throw new RuntimeException("Disk is full"),
];

foreach ($tests as $test) {
    try {
        $test();
    } catch (Throwable $e) {
        echo get_class($e), ": ", $e->getMessage(), "\n";
    }
}
Ожидаемый результат
DivisionByZeroError: Division by zero
JsonException: Syntax error
RuntimeException: Disk is full

Собственные исключения

Чтобы создать собственный класс исключения, унаследуйся от существующего — обычно от RuntimeException или DomainException. Пусть имя ясно описывает проблему, а дополнительные данные при необходимости хранятся в свойстве. Тогда блок catch сможет точно отреагировать именно на эту ситуацию.

PHP
<?php

class InsufficientFundsException extends RuntimeException
{
    public function __construct(public readonly float $missing)
    {
        parent::__construct("Not enough money: missing $missing AZN");
    }
}

class Wallet
{
    public function __construct(private float $balance) {}

    public function pay(float $amount): void
    {
        if ($amount > $this->balance) {
            throw new InsufficientFundsException($amount - $this->balance);
        }
        $this->balance -= $amount;
        echo "Paid $amount, left {$this->balance}\n";
    }
}

$wallet = new Wallet(30);
try {
    $wallet->pay(12);
    $wallet->pay(25.5);
    $wallet->pay(1);
} catch (InsufficientFundsException $e) {
    echo $e->getMessage(), "\n";
    echo "Top up at least ", ceil($e->missing), " AZN\n";
}
Ожидаемый результат
Paid 12, left 18
Not enough money: missing 7.5 AZN
Top up at least 8 AZN
Старый стиль: проверка возвращаемого значения
$data = json_decode($input);
if ($data === null) {
    // broken JSON or the value null?
    echo json_last_error_msg();
}
Современно: исключение
try {
    $data = json_decode($input, flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    echo "Invalid JSON: ", $e->getMessage();
}
null может означать и ошибку, и корректное JSON-значение (null). Исключение же сообщает об ошибке однозначно.

Предупреждения и вывод ошибок

Некоторые проблемы не выбрасывают исключение, а лишь выдают предупреждение: например, несуществующая переменная даёт Warning: Undefined variable $x, а устаревшая функция — сообщение Deprecated. Какие сообщения будут видны, определяют настройки php.ini. Золотое правило: во время разработки видеть всё, а в продакшене (production) ничего не показывать пользователю и всё записывать в журнал.

Настройка (php.ini)РазработкаПродакшен
error_reportingE_ALLE_ALL
display_errorsOnOff
log_errorsOnOn
Включить можно и в коде: error_reporting(E_ALL); ini_set('display_errors', '1');

Главное

  • throw выбрасывает исключение; try содержит опасный код, catch перехватывает конкретный тип, а finally выполняется всегда.
  • У Throwable две ветви: Exception (ожидаемые проблемы) и Error (внутренние ошибки PHP).
  • Наследуй собственное исключение от существующего класса и давай ему понятное имя; дополнительные данные храни в свойстве.
  • В PHP 8 throw — выражение: $x = $arr[$k] ?? throw new ...;
  • При разработке — display_errors = On, а в продакшене — Off вместе с log_errors = On.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Когда выполняется блок finally?