W3docs

PHP Exception

Исключения PHP: как выбрасывать и перехватывать ошибки с try/catch/finally, методы класса Exception, множественный catch и собственные классы исключений.

Что такое исключение?

Исключение — это объект, представляющий ошибку или неожиданное условие, прерывающее нормальный ход выполнения программы. Вместо того чтобы позволить проблеме бесшумно завершить скрипт, вы выбрасываете исключение в точке возникновения ошибки, а затем перехватываете его там, где можно восстановиться, записать в журнал или сообщить об ошибке.

На этой странице рассматривается, как выбрасывать и перехватывать исключения с помощью try/catch/finally, какие методы предоставляет каждый объект исключения, как перехватывать несколько типов исключений и как писать собственные классы исключений.

Исключения удобны, когда функция не может продолжить работу осмысленно: некорректный пользовательский ввод, неудачное подключение к базе данных, отсутствующий файл или значение вне допустимого диапазона. Они отделяют логику обработки ошибок от нормальной логики, благодаря чему «счастливый путь» остаётся читаемым.

throw new Exception('Something went wrong');

В PHP 7 и более поздних версиях и Exception, и Error реализуют интерфейс Throwable, поэтому всё, что можно выбросить, является Throwable.

Выброс и перехват с помощью try / catch / finally

Рискованный код оборачивается в блок try. Если инструкция внутри выбрасывает исключение, PHP прекращает выполнение оставшейся части блока try и переходит к первому совпадающему catch. Необязательный блок finally всегда выполняется после — независимо от того, было ли выброшено исключение, — поэтому он является подходящим местом для освобождения ресурсов (закрытия файла, дескриптора базы данных, блокировки).

<?php
function divide($a, $b) {
    if ($b === 0) {
        throw new InvalidArgumentException('Division by zero is not allowed.');
    }
    return $a / $b;
}

try {
    echo divide(10, 2), "\n";   // 5
    echo divide(10, 0), "\n";   // throws — the next line never runs
} catch (InvalidArgumentException $e) {
    echo 'Caught: ' . $e->getMessage() . "\n";
} finally {
    echo "Done.\n";
}

Вывод:

5
Caught: Division by zero is not allowed.
Done.

Обратите внимание, что второй вызов echo divide(...) не выводится, потому что выброс исключения немедленно прерывает блок try. Блок finally при этом всё равно выполняется.

Получение информации из исключения

Каждый объект исключения содержит полезные данные. Наиболее распространённые методы, все унаследованные от базового класса Exception:

МетодВозвращает
getMessage()Удобочитаемое сообщение об ошибке
getCode()Целочисленный код ошибки, переданный в конструктор
getFile()Файл, в котором было создано исключение
getLine()Номер строки, в которой оно было создано
getTraceAsString()Стек вызовов в виде строки, полезен для журналирования
getPrevious()Предыдущее исключение, когда одно оборачивает другое
<?php
class InsufficientFundsException extends Exception {}

class Account {
    private float $balance;
    public function __construct(float $balance) { $this->balance = $balance; }

    public function withdraw(float $amount): void {
        if ($amount > $this->balance) {
            throw new InsufficientFundsException(
                "Cannot withdraw $amount; balance is {$this->balance}.",
                100 // a custom error code
            );
        }
        $this->balance -= $amount;
    }
}

$account = new Account(50);
try {
    $account->withdraw(80);
} catch (InsufficientFundsException $e) {
    echo $e->getMessage() . "\n";  // Cannot withdraw 80; balance is 50.
    echo 'Code: ' . $e->getCode() . "\n"; // Code: 100
}

Вывод:

Cannot withdraw 80; balance is 50.
Code: 100

Перехват нескольких типов исключений

Один блок try может иметь несколько блоков catch, которые проверяются сверху вниз — побеждает первый, тип которого совпадает с выброшенным исключением. Если два не связанных между собой типа исключений должны обрабатываться одинаково, объедините их в одном блоке с помощью оператора | (pipe) вместо дублирования кода.

<?php
function parseAge(string $input): int {
    if (!is_numeric($input)) {
        throw new TypeError("'$input' is not a number.");
    }
    $age = (int) $input;
    if ($age < 0) {
        throw new RangeException("Age cannot be negative.");
    }
    return $age;
}

foreach (['42', 'abc', '-5'] as $value) {
    try {
        echo parseAge($value) . "\n";
    } catch (TypeError | RangeException $e) {
        echo get_class($e) . ': ' . $e->getMessage() . "\n";
    }
}

Вывод:

42
TypeError: 'abc' is not a number.
RangeException: Age cannot be negative.

Порядок имеет значение: поскольку исключения PHP образуют иерархию, ставьте более специфичные типы первыми, а более общие (например, Exception или Throwable) — последними, иначе общий блок перехватит всё раньше, чем специфичный получит шанс.

Создание пользовательских исключений

Помимо встроенных классов, вы можете определять собственные типы исключений, расширяя Exception (или более специфичный встроенный класс, например RuntimeException). Выделенный класс делает блоки catch выразительными — вы можете реагировать именно на вашу ошибку — и позволяет добавлять дополнительные данные.

В примере с Account выше InsufficientFundsException является пользовательским исключением. Зачастую достаточно пустого подкласса; добавляйте методы только тогда, когда нужно дополнительное поведение:

<?php
class ValidationException extends Exception {
    private array $errors;

    public function __construct(string $message, array $errors = []) {
        parent::__construct($message);
        $this->errors = $errors;
    }

    public function getErrors(): array {
        return $this->errors;
    }
}

Если вы переопределяете конструктор, всегда вызывайте parent::__construct(), чтобы сообщение, код и предыдущее исключение были инициализированы корректно.

Перехват Throwable. Чтобы обрабатывать как обычные исключения, так и ошибки уровня движка (например, TypeError при неверном типе аргумента), перехватывайте Throwable. Это наиболее широкий «catch-all», но используйте его в крайнем случае, чтобы случайно не скрыть баги, которые нужно исправить:

try {
    // risky code
} catch (Throwable $e) {
    error_log($e->getMessage());
}

Лучшие практики

  • Перехватывайте только то, что можете обработать. Позвольте исключениям, от которых нельзя восстановиться, распространяться вверх до централизованного обработчика.
  • Выбрасывайте рано, перехватывайте поздно. Выбрасывайте исключение в точке, где значение становится некорректным; перехватывайте там, где реально можете на него отреагировать.
  • Используйте специфичные типы. Пользовательские или встроенные подклассы лучше, чем голый Exception для управления потоком.
  • Пишите информативные сообщения и используйте getCode() для машиночитаемой категоризации.
  • Журналируйте, не замалчивайте. Записывайте исключения с помощью error_log() вместо их подавления. Для неперехваченных исключений регистрируйте глобальный обработчик с помощью set_exception_handler().
  • Не используйте исключения для обычного потока. Оставьте их для действительно исключительных ситуаций.

Поток управления

Диаграмма ниже показывает путь выполнения через структуру try/catch/finally:

graph TD
  Try[Try Block] -->|No Exception| Finally[Finally Block]
  Try -->|Exception Thrown| Catch[Catch Block]
  Catch --> Finally

Практика

Практика
Что верно об обработке исключений в PHP?
Что верно об обработке исключений в PHP?
Was this page helpful?