company_banner

Где ошибка, Билли? Нам нужна ошибка…


    Некоторое время назад мой коллега опубликовал статью про обработку ошибок в Java/Kotlin. И мне стало интересно, а какие вообще в программировании существуют способы передачи ошибок. Если вам тоже интересно, то под катом результат изысканий. Скорее всего, какие-то экзотические методы пропущены, но тут одна надежда на комментарии, которые на Хабре порою бывают интереснее и полезнее самой статьи. :)

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

    Disclaimer: для краткости и упрощения восприятия для любого изолированного выполняющегося кода, порождающего ошибку, я буду использовать слово «функция», а для любой не примитивной (integer, string, boolean, etc…) сущности — «структура».

    Непосредственный возврат


    С непосредственным возвратом всё просто. Хотя это, наверное, самый часто используемый способ, но и тут есть множество вариантов. Объединяет их всех способ обработки — сравнение возвращаемого значения с предопределёнными значениями.

    1. Возврат статуса выполнения. Самый банальный вариант — TRUE (если исполнилось штатно) или FALSE (если произошёл сбой).
    2. Возврат корректного значения в случае успеха и некорректного в случае ошибки.
      C/C++
      Функция strchr() возвращает указатель на первое вхождение символа ch в строку, на которую указывает str. Если символ ch не найден, возвращается NULL.

      Довольно часто подходы 1 и 2 используют совместно с выставлением состояния.
    3. Возврат кода ошибки. Если мы хотим не просто знать, что исполнение завершилось некорректно, но и понять, в каком месте функции произошла ошибка. Обычно, если функция завершилась без ошибки, возвращается код 0. В случае же ошибки — код, по которому можно определить конкретное место в теле функции, где что-то пошло не так. Но это не железное правило, посмотрите, например, на HTTP с его 200.
    4. Возврат кода ошибки в некорректном диапазоне значений. Например, штатно функция должна вернуть целое положительное, а в случае ошибки — её код со знаком минус.

      function countElements(param) {
          if (!isArray(param)) {
              return -10;
          } else if(!isInitialized(param)){
              return -20
          } else {
              return count(array);
          }
       }

    5. Возврат разных типов для положительного и отрицательного результатов. Например, штатно — строка, а не штатно — число или класс Success и класс Error.

      sealed class UserProfileResult {
          data class Success(val userProfile: UserProfileDTO) : UserProfileResult()
          data class Error(val message: String, val cause: Exception? = null) : UserProfileResult()
      }
      
      val avatarUrl = when (val result = client.requestUserProfile(userId)) {
          is UserProfileResult.Success -> result.userProfile.avatarUrl
          is UserProfileResult.Error -> "http://domain.com/defaultAvatar.png"
      }

      Также можно вспомнить Either из мира функционального программирования. Хотя тут можно и поспорить.
    6. Возврат структуры, содержащей как сам результат, так и ошибку.

      function doSomething(): array {
          ...
          if($somethingWrong === true) {
              return ["result" => null, "error" => "Alarm!!!"];
          } else {
              return ["result" => $result, "error" => null];
          }
          ...
      }

    7. Возврат нескольких значений. Сначала я был склонен не отделять этот способ от предыдущего, но в итоге всё же решил вынести в отдельный пункт. Этот вариант встречается довольно редко, поскольку может использоваться исключительно в языках, которые позволяют возвращать из функции несколько значений, а таковых не то чтобы много. Самым ярким, но не единственным примером может служить язык Go.

      f, err := Sqrt(-1)
      if err != nil {
          fmt.Println(err)
      }

    Выставление состояния


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

    1. Выставление «глобального» состояния. В кавычки взял потому, что чаще всего речь всё же идёт о глобальности в некоторой области видимости.

      # ls /unknown/path 2>/dev/null
      # echo $?
      1

    2. Выставление собственного состояния. Когда у нас есть некоторая структура, предоставляющая функцию. Функция выставляет состояние для этой структуры, а ошибка извлекается уже из структуры либо непосредственно, либо с помощью другой специализированной функции.

      $mysqli = new mysqli("localhost", "my_user", "my_password", "world");
      $result = $mysqli->query("SET a=1");
      if ($mysqli->errno) {
          printf("Код ошибки: %d\n", $mysqli->errno);
      }

    3. Выставление состояния возвращённого объекта. Сильно перекликается с п.6. из предыдущего раздела. В отличии от предыдущего пункта, проверка состояния производится у возвращённой структуры, а не у предоставляющей функцию. Как очевидный пример можно привести протокол HTTP и бесчисленное количество библиотек на самых разных языках, с ним работающих.

      Response response = client.newCall("https://www.google.com").execute();
      Integer errorCode = response.getCode();


    Передача управления


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

    1. Исключения. Всем известный throw/try/catch. Бросая исключение, функция формирует структуру, описывающую ошибку и, чаще всего, содержащую разные полезные метаданные, облегчающие диагностику возникшей проблемы (например стек вызовов). После чего эта структура передается специальному механизму, который «откатывается» по стеку вызовов до первого блока try, которому сопоставлен catch, умеющий обрабатывать исключения такого типа. Хорош этот способ тем, что вся логика проброса исключения осуществляется самой средой исполнения. Плох тем же, так как накладные расходы (давайте только без холиваров :)).
    2. Глобальные обработчики ошибок. Не самый распространённый способ, но встречается. Даже не знаю, что тут рассказывать. Разве что отметить, что сюда же можно отнести и механизмы обозревателей: когда работающий в стороне от основного потока код следит за поступающими в него событиями.

      function myErrorHandler($errno, $errstr, $errfile, $errline) {
          echo "<b>Custom error:</b> [$errno] $errstr<br>";
          echo " Error on line $errline in $errfile<br>";
      }
      
      set_error_handler("myErrorHandler");

    3. Колбэки. Нежно любимы разработчиками под Android, на JavaScript и апологетами реактивного программирования. Суть проста: в функцию, кроме обрабатываемых данных, передаются функции-обработчики. В случае возникновения ошибки основная функция вызовет соответствующий обработчик и передаст ему возникшую ошибку.

      var observer = Rx.Observer.create(
        x => console.log(`onNext: ${x}`),
        e => console.log(`onError: ${e}`),
        () => console.log('onCompleted'));


    Вроде бы ничего не забыл.

    И забавный факт. Наверное, самый оригинальный способ возврата ошибки, сочетающий в себе одновременно исключения, выставление состояния и возврат нескольких значений, мне встречался в Informix SPL (пишу по памяти):

    CREATE PROCEDURE some_proc(...)
    RETURNING int, int, int, int;
    …
    ON EXCEPTION SET SQLERR, ISAMERR
        RETURN 0, SQLERR, ISAMERR, USRERR;
    END EXCEPTION;
    
    LET USRERR = 1;
    -- do Something That May Raise Exception
    LET USRERR = 2;
    -- do Something Other That May Raise Exception
    …
    RETURN result, 0, 0, 0;
    END PROCEDURE
    • +25
    • 5,4k
    • 8
    FunCorp
    152,35
    Разработка развлекательных сервисов
    Поделиться публикацией

    Похожие публикации

    Комментарии 8

      +3

      Either отличается от возврата структуры куда сильнее, чем возврат нескольких значений. Основное отличие — в том, что Either содержит значение или ошибку, а никак не их вместе. Ещё одно отличие — в тех языках, где используют Either, как правило существуют удобные способы работы с ним.

        0

        Ваша правда, промахнулся пунктом, когда комментарий про Either вставлял.

          –1
          Пример: Enums в Rust.
            0
            Любопытно, за что минус? Привёл неверную аналогию?
          +1

          "… экзотические методы пропущены..." — алгебраические эффекты. Интересная штука. Недавно статья была на Хабре, советую

            0

            Ссылкой не поделитесь? :)

            0
            Да, идея хорошая. Выглядит как некий DI средствами самого языка.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое