Если видео недоступно для просмотра, попробуйте выключить блокировщик рекламы.

Отдельная большая тема в программировании – обработка ошибок. До сих пор, нам удавалось избегать её, но в реальном мире, где приложения содержат тысячи, десятки и сотни тысяч (а то и все миллионы) строк кода, обработка ошибок влияет на многое: простоту модификации и расширения, адекватное поведение программы для пользователя в разных ситуациях.

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

В PHP есть функция, которая называется strpos($text, $substr). Она ищет подстроку $substr внутри текста $text и возвращает индекс начала этой подстроки в тексте. Что произойдет если подстрока не была найдена? Является ли это поведение ошибкой? Нет. Это штатное поведение функции. От того, что подстрока не была найдена, ничего страшного не случилось. Представьте себе любой редактор текста и механизм поиска внутри него. Ситуация когда ничего не было найдено, возникает постоянно, и это не ломает работу программы.

Кстати, посмотрите в документацию этой функции, каким образом она говорит о том, что подстрока не была найдена?

Другая ситуация. В тех же редакторах есть функция "открыть файл". Представьте, что во время открытия файла, что-то пошло не так, например, его удалили. А это ошибка или нет? Да, в этой ситуации произошла ошибка, но это не ошибка программирования. Подобная ошибка может возникуть всегда, независимо от желания программиста. Он не может избежать ее появления. Единственное что он может, правильно реализовать её обработку.

Еще один интересный вопрос, насколько это критичная ошибка? Должно ли оно приводить к остановке всего приложения или нет? В плохо написанных приложениях, там где неправильно реализована обработка ошибок, такая ситуация приведет к краху всего приложения, и оно завершится. В хорошо написанном приложении не произойдет ничего страшного. Пользователь увидит предупреждение о том что, файл не читается, и сможет выбрать дальнейшие действия, например, попытаться прочитать его снова или выполнить другое действие.

Сказанное выше имеет очень серьезные следствия. Одна и та же ситуация на разных уровнях может как являться ошибкой, так и быть вполне штатной ситуацией. Например, если задача функции читать файл, а она не смогла этого сделать, то с точки зрения этой функции произошла ошибка. Должна ли она приводить к остановке всего приложения? Как мы выяснили выше – не должна. О том, насколько критична данная ситуация, может решать приложение, которое использует эту функцию, но не сама функция.

Коды возврата

В языках появившихся до 1990 года (примерно), обработка ошибок выполнялась через механизм возврата функцией специального значения. Например, в СИ, если функция не может выполнить свою задачу, то она должна вернуть специальное значение, либо NULL либо отрицательное число. Значение этого числа, говорит о том, какая ошибка произошла. Например:

int write_log()
{
    int ret = 0; // return value 0 if success
    FILE *f = fopen("logfile.txt", "w+");

    // Проверяем, получилось ли открыть файл
    if (!f)
        return -1;

    // Проверяем, что не достигли конца файла
    if (fputs("hello logfile!", f) != EOF) {
        // continue using the file resource
    } else {
        // Файл закончился
        ret = -2;
    }

    // Не получилось закрыть файл
    if (fclose(f) == EOF)
        ret = -3;

    return ret;
}

Обратите внимание на условные конструкции и постоянное присваивание переменной ret. Фактически каждая потенциально опасная операция, должна проверяться на успешность выполнения. Если что-то пошло не так, то функция возвращает специальный код.

И вот тут начинаются проблемы. Как показывает жизнь, в большинстве ситуаций, ошибка обрабатывается не там где она возникла и даже не уровнем выше. Предположим, что есть функция A, которая вызывает код, потенциально приводящий к ошибке, и она его должна уметь правильно обработать и сообщить пользователю о проблеме. При этом сама ошибка происходит внутри функции E, которая вызывается внутри A не напрямую, а через цепочку функций: A => B => C => D => E. Подумайте, к чему приводит такая схема? Все функции в этой цепочке, даже не смотря на то что они не обрабатывают ошибку, обязаны знать про нее, отлавливать ее и так же возвращать наружу код этой ошибки. В результате, количество кода, который занимается ошибками, становится так много, что за ним теряется код, выполняющий исходную задачу.

Стоит сказать, что существуют схемы обработки ошибок, которые не обладают такими недостатками, но работают по принципу возврата. Например монада Either.

Исключения

Именно на этом фоне и возник механизм исключений. Его главная цель, передать ошибку из того места, где она возникла, в место, где ее можно обработать, минуя все промежуточные уровни. Другими словами, механизм исключений раскручивает стек вызовов самостоятельно.

С исключениями нужно запомнить две вещи: код, в котором произошла ошибка бросает исключение, код, в котором ошибка обрабатывается – ловит.

<?php

// Функция которая может бросить исключение
function readFile($filepath) {
    if (!is_readable($filepath)) {
        throw new \Exception("'{$filepath}' is not readble");
    }
    // ...
}

// Где-то в другом месте программы

function run()
{
    try {
        // Функция, которая вызывает readFile. Возможно не напрямую, а через другие функции.
        // Для механизма исключений это не важно.
        openFile($filepath);
    } catch (\Exception $e) {
        // Этот блок выполняется только в одном случае, если в блоке try было "возбуждено" (выброшено) исключение
        showErrorToUser($e);
    }
    // Если тут будет код, он продолжит выполняться
}

Сами исключения – это объекты класса \Exception и его наследников (о наследовании в одном из следующих курсов). Этот объект содержит внутри себя сообщение, переданное в конструктор, бектрейс и другие полезные данные:

<?php

Exception implements Throwable {
    protected string $message ;
    protected int $code ;
    protected string $file ;
    protected int $line ;

    public __construct ([ string $message = "" [, int $code = 0 [, Throwable $previous = NULL ]]] )
    final public getMessage ( void ) : string
    final public getPrevious ( void ) : Throwable
    final public getCode ( void ) : mixed
    final public getFile ( void ) : string
    final public getLine ( void ) : int
    final public getTrace ( void ) : array
    final public getTraceAsString ( void ) : string
    public __toString ( void ) : string
}

Бросить исключение проще простого, достаточно использовать инструкцию throw:

<?php

$e = new \Exception('Тут любой текст');
throw $e; // исключение можно создать отдельно, а можно сразу же там, где используется throw

throw прерывает дальнейшее выполнение кода. В этом смысле оно подобно return, но в отличии от него, прерывает выполнение не только текущей функции, но и всего кода, вплоть до ближайщего в стеке вызовов блока catch.

Блок try/catch обычно ставится на самом верхнем уровне программы, но это не обязательно. Вполне вероятно, что есть несколько промежуточных блоков, которые могут отлавливать ошибки и снова их возбуждать. Эта тема достаточно сложная и требует некоторого опыта работы.

PHP

Когда PHP только появился, то в нём не был реализован механизм исключений. Поэтому многие функции либо возвращают какие-то значения указывающие на ошибку, либо об ошибке можно узнать с помощью специальных ухищрений. Эти подходы пришли прямиком из си и в современных языках больше нигде не встречаются. Все еще ухудшается наличием альтернативного механизма: Notice, Warning, Fatal. К счастью в новых версиях PHP эти ошибки заменяют на исключения, но вряд ли когда-нибудь произойдет полный переход. Написано слишком много кода, и язык должен сохранять обратную совместимость.

Например, обработка ошибок парсинга json в PHP, реализована крайне странно, по современным меркам. Чтобы узнать об ошибке, нужно вызвать функцию json_last_error() сразу после парсинга. А текст ошибки получается функцией json_last_error_msg().


Дополнительные материалы

  1. Официальная документация
  2. Коды возврата & исключения
  3. Библиотека safe
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →