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

finally и свои исключения

У курьера есть журнал обхода. Посылку вручили, оставили записку или понесли обратно — что бы ни случилось у двери, курьер расписывается в журнале. Без подписи никто не узнает, на какой квартире закончился обход.

В коде тоже бывают действия, которые нужны при любом исходе: закрыть файл, освободить ресурс, записать «готово». Если поставить такую строку после try и catch, она не выполнится, когда в try сработает return или исключение останется непойманным. Для этого у try есть третий блок — finally.

Блок finally

Блок finally пишут после try и блоков catch, а catch можно и не писать вовсе. PHP выполняет finally при любом выходе из try:

  1. Блок try дошёл до конца — finally выполняется следом.
  2. В try или catch сработал return — значение откладывается, выполняется finally, и только потом функция возвращает отложенное значение.
  3. Брошено исключение — finally выполняется после подходящего catch, а если такого нет, перед тем как исключение уйдёт выше.

Давайте проследим за функцией, которая выходит из try двумя путями: через return и через throw. Нажимайте «Следующий шаг» и следите, на каком шаге печатается finally и когда echo получает результат.

Вживуюfinally после return и после catchшаг 1 из 12 · рамок: 1

Чтобы напечатать результат, PHP вызывает parse('42').

Следите, когда печатается finally и когда echo получает результат

Программа напечатала finally, 42, caught, finally, 0. В обоих вызовах finally сработал раньше, чем echo получил результат:

  1. Для '42' оператор return отложил 42, finally напечатал свою строку, и только потом echo напечатал число.
  2. Для пустой строки throw прервал try, catch напечатал caught и отложил 0, затем снова сработал finally.

Частый вопрос на собеседовании: выполнится ли finally, если в try стоит return. Да: return откладывает значение, finally выполняется, и только потом функция его возвращает. Пропустить finally может настоящая фатальная ошибка, например нехватка памяти: она останавливает программу сразу.

Здесь картинка с журналом перестаёт работать. Курьер расписывается после доставки, а finally выполняется до того, как значение из return дойдёт до вызывающего кода.

Свои классы исключений

По тексту сообщения сбои трудно различать: текст меняют, а catch его не читает. Надёжнее дать сбою свой класс. Свой класс исключения наследует один из готовых классов, и пустого тела достаточно: конструктор с сообщением и кодом, getMessage и getCode достаются от родителя.

PHP
<?php

class OutOfStockException extends RuntimeException {}

function reserve(int $stock, int $wanted): int
{
    if ($wanted > $stock) {
        throw new OutOfStockException("only $stock left", 42);
    }
    return $stock - $wanted;
}

Поймать такое исключение можно и по его классу, и по любому родителю:

PHP
try {
    echo reserve(5, 2), "\n"; // 3
    echo reserve(5, 9), "\n";
} catch (RuntimeException $e) {
    echo get_class($e), "\n"; // OutOfStockException
    echo $e->getCode(), ' ', $e->getMessage(), "\n"; // 42 only 5 left
}

OutOfStockException — потомок RuntimeException, поэтому catch (RuntimeException $e) его ловит. Объект при этом не меняется: get_class($e) называет настоящий класс, а сообщение и код 42 остались те, что передали в new.

Передать дальше: throw $e

Иногда функция может отметить сбой, но не исправить его. Тогда в catch она записывает, что случилось, и бросает тот же объект снова — throw $e:

PHP
function buy(int $wanted): int
{
    try {
        return reserve(5, $wanted);
    } catch (OutOfStockException $e) {
        echo 'log: ', $e->getMessage(), "\n"; // log: only 5 left
        throw $e;
    }
}

try {
    buy(9);
} catch (RuntimeException $e) {
    echo 'Sorry, sold out', "\n"; // Sorry, sold out
}

buy напечатала строку с log:, как будто записала в журнал, и отправила исключение дальше вверх. Его поймал внешний catch и показал покупателю понятное сообщение.

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

Свой класс исключения объявляют без extends:

PHP
<?php

class OutOfStockException {}

throw new OutOfStockException('only 5 left'); // ✗
TEXT
Fatal error: Uncaught Error: Cannot throw objects that do not implement Throwable

Бросать можно только объект, который реализует Throwable, а обычный класс его не реализует. Добавьте родителя из ветви Exception: class OutOfStockException extends RuntimeException {}.

Главное

  • finally выполняется при любом выходе из try: после конца блока, после return и после исключения.
  • Значение из return ждёт, пока finally закончится, и только потом уходит вызывающему коду.
  • Свой класс исключения наследует Exception или его потомка; сообщение и код он получает от родителя.
  • catch по родительскому классу ловит и потомков. get_class($e) называет настоящий класс. throw $e передаёт исключение дальше.