Перейти к содержимому
Теория · Ошибки и исключения · урок 2 из 8

try, catch и throw

В прошлом уроке брошенный объект DivisionByZeroError долетел до самого верха, и программа остановилась. Вспомним курьера: посылка, которую не вручили, идёт вверх по лестнице, пока кто-нибудь не откроет дверь. В коде такая дверь — блок catch, и поставить её можно там, где вы знаете, что делать со сбоем.

Бросать объекты умеет не только PHP, но и ваш код. Объект, который код бросает, чтобы сообщить о сбое, называется исключением. Для этого есть готовые классы, например InvalidArgumentException — «неправильный аргумент».

throw: бросить исключение

Пусть функция снимает деньги со счёта и не может уйти в минус:

PHP
<?php

function withdraw(int $balance, int $amount): int
{
    if ($amount > $balance) {
        throw new InvalidArgumentException('not enough money');
    }
    return $balance - $amount;
}
try {
    echo withdraw(100, 30), "\n"; // 70
    echo withdraw(100, 500), "\n";
} catch (InvalidArgumentException $e) {
    echo 'error: ', $e->getMessage(), "\n"; // error: not enough money
}

Разберём по шагам:

  1. throw new InvalidArgumentException('not enough money') создаёт объект с сообщением и бросает его.
  2. Первый вызов проходит проверку и возвращает 70.
  3. Во втором вызове условие истинно. throw прерывает функцию: до return дело не доходит, и значения у вызова нет.
  4. Исключение покидает withdraw и попадает в код, который её вызвал. Строка с echo в блоке try не завершается.
  5. PHP проверяет catch: класс подходит, объект попадает в переменную $e, и выполняется тело catch.

Если бы вокруг вызова не было try, исключение пошло бы дальше: к тому, кто вызвал этот код, потом к следующему, и так до самого верха. Цепочку вызовов, которые ещё не закончились, называют стеком, а такой путь назад по ней — размоткой стека. На собеседовании любят спрашивать, что выполнится после throw: в блоке try — уже ничего, а после блока catch программа идёт дальше как обычно.

Блок catch

Пойманное исключение — это значение

В переменной $e лежит объект, и у него можно спросить подробности:

  • $e->getMessage() — сообщение, переданное при создании;
  • $e->getCode() — числовой код ошибки, если его передали вторым аргументом, иначе 0.

Первый подходящий catch

У одного try может быть несколько блоков catch. PHP проверяет их сверху вниз и выполняет первый, чей класс подходит:

PHP
<?php

try {
    throw new RuntimeException('disk is full', 507);
} catch (InvalidArgumentException $e) {
    echo 'bad argument', "\n";
} catch (Exception $e) {
    echo $e->getMessage(), ' ', $e->getCode(), "\n"; // disk is full 507
}
echo 'next line', "\n"; // next line

RuntimeException не подходит под InvalidArgumentException, поэтому первый catch пропущен. Второй подходит: catch (Exception $e) ловит и класс Exception, и его дочерние классы, а RuntimeException и InvalidArgumentException — оба его потомки. Поэтому конкретные классы ставят выше, а общий Exception — ниже.

Здесь картинка с дверью перестаёт работать. Настоящую дверь откроют любому курьеру, а catch открывается только для своего класса и его потомков.

Несколько классов в одном catch

Когда разные сбои обрабатываются одинаково, их классы перечисляют через |:

PHP
<?php

$people = 0;
try {
    echo intdiv(100, $people), "\n";
} catch (InvalidArgumentException | DivisionByZeroError $e) {
    echo 'cannot share: ', $e->getMessage(), "\n"; // cannot share: Division by zero
}

intdiv бросила DivisionByZeroError, и его поймал тот же catch, что ловит InvalidArgumentException. Вместо остановки программа напечатала сообщение.

Частая ошибка

После throw забывают слово new:

PHP
<?php

$name = '';
if ($name === '') {
    throw InvalidArgumentException('empty name'); // ✗
}
TEXT
Fatal error: Uncaught Error: Call to undefined function InvalidArgumentException()

Без new PHP читает InvalidArgumentException('empty name') как вызов функции с таким именем. Такой функции нет, и PHP бросает свою ошибку вместо вашей. Бросать можно только объект, поэтому нужно throw new InvalidArgumentException('empty name');.

Главное

  • throw new … бросает исключение: оставшиеся строки блока try и функции не выполняются.
  • Исключение идёт назад по цепочке вызовов до первого подходящего catch, а после catch программа продолжает работу.
  • catch (Exception $e) ловит Exception и все его дочерние классы; блоки catch проверяются сверху вниз.
  • $e->getMessage() и $e->getCode() рассказывают, что случилось; catch (A | B $e) ловит два класса сразу.