Для чего нужен finally в try…catch

В PHP, начиная с версии 5.5, а в JavaScript с редакции 1.4 есть поддержка обработки с исключений с помощью конструкций try…catch…finally.

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

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

try { 
    echo "В try вызвано исключение\n";
    throw new StrangeException();
} catch (SomeException $e) {
    echo "Вызвано исключение SomeException \n";
} catch (AnotherException $e) {
    echo "Вызвано исключение AnotherException \n";
} finally {
    echo "Этот блок кода выполнится всегда\n";
}

То есть мы можем как-то среагировать на такую кажущуюся «неуправляемой» ситуацию перед тем, как исключение уйдет на верхний уровень.

В случае вложенных исключений, если блоки finally в них присутствуют, то код будет выполнен во всех таких блоках последовательно, начиная с самого «глубокого» уровня. Например:

function canThrowException() {
    try { 
        echo "В try вызвано исключение\n";
        throw new StrangeException("Что-то случилось!");
    } catch (SomeException $e) {
        echo "Exception: " . $e->getMessage() . "\n";
    } finally {
        echo "Этот блок в функции canThrowException отработает сначала!\n";
    }
}
try {
    canThrowException();
} catch (StrangeException $e) {
    echo "Внешнее исключение: " . $e->getMessage() . "\n";
} finally {
    echo "Это блок кода также отработает, но в конце\n";
}

Блоки finally могут быть полезны при соединении с базой данных или со сторонним API, когда, например, у вас может быть многоэтапная обработка исключений, но при этом важно при любом исходе закрыть соединение или какой-то ресурс — такие процедуры безусловного «завершения» или «очистки» как раз удобно размещать в finally.

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

Как уже было отмечено, в finally можно поместить операции «завершения» чего-либо. Например, это очень удобно делать для закрытия файловых потоков или «подчистку» после обращений к внешним API.

Например, для закрытия потоков хороший пример написан в статье Frank de Jonge:

Код без с использования finally:

function flupper($location) {  
   $handle = fopen($location, 'r+');
   $firstLine = fgets($handle, 1024);

   if ($firstLine === false) {
       fclose($firstLine);

       return false;
   }

   rewind($handle);
   $uppedFirstLine = strtoupper($firstLine);

   if (false === fwrite($handle, $uppedFirstLine)) {
       fclose($firstLine);

       return false;
   }

   fclose($firstLine);

   return $uppedFirstLine;
}

Код с использованием finally:

function flupper($location) {  
    try {
        $handle = fopen($location, 'r+');
        $firstLine = fgets($handle, 1024);

        if ($firstLine === false) {
            return false;
        }

        rewind($handle);
        $uppedFirstLine = strtoupper($firstLine);

        if (false === fwrite($handle, $uppedFirstLine)) {
            return false;
        }

        return $uppedFirstLine;
    } finally {
        fclose($handle);
    }
}

То есть мы избавились от копипасты закрытия потока, и проводим эту рутинную операцию в одном месте.

 

5 комментариев

  1. Вдогонку… Понял. Блок catch (\Throwable $e) исполнится только при условии выброса исключения. А finally — в любом случае.

    А вариант написать нужный нам код просто под низом конструкции try/catch не будет выполняться, если было выброшено исключение. Таким образом, если нам нужно что-то сделать как при успешном выполнении try-кода, так и при выбросе исключения, единственный выход, чтобы сделать это красиво — блок finally.

    1. не совсем так: если сделать catch(Throwable), и при этом будет выброшено любое исключение, то код под блоком выполнится и в этом случае finally можно не использовать. Или наоборот, использовать finally без catch.

      а вот если нужно поймать какое-то определенное исключение и впихнуть особенную логику его обработки, а в конце выполнить какое-то действие (запись в бд или, как у автора, закрыть соединение), то finally будет полезно.

      Вместо
      «`php
      function some() {
      throw new \DomainException(‘Specified exception’);
      }

      try {
      some();

      $model->status = ‘successful’;
      $model->save();
      } catch (\DomainException $exception) {
      $model->status = ‘failed’;
      $model->message = $exception->getMessage();
      $model->save();
      }
      «`

      Можно использовать такую конструкцию:
      «`php
      try {
      some();

      $model->status = ‘successful’;
      } catch (\DomainException $exception) {
      $model->status = ‘failed’;
      $model->message = $exception->getMessage();
      } finally {
      $model->save();
      }
      «`
      В первом варианте мы сохраняем модель только в случае если не было исключений или поймали исключение DomainException.
      А во втором варианте мы сохраняем модель в любом случае.

      Еще стоит учитывать нюанс поимки исключений. Не помню с какой версии php они разделились на глобальный Throwable и Exception
      «`php
      try {
      $result = 2 / 0;
      } catch (\Exception $exception) {
      echo ‘Деление на ноль’;
      }
      «`
      эта штука НЕ сработает, будет выведен warning. А вот если вместо Exception написать Throwable, то исключение поймается

  2. «то есть это пригодится для обработки ситуаций, когда блок catch не отработал, а исключение возникло»
    Почему нельзя finally заменить на catch (\Throwable $e) ? Который точно подхватит все возможно оставшиеся не перехваченными исключения в предыдущих блоках catch.

Добавить комментарий для Антон Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.