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

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

Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».

Skillbox рекомендует: Образовательный онлайн-курс «Профессия Java-разработчик».


Не всегда стоит рубить с плеча


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

  • У вас есть диапазон дат, где «до» находится перед «от»? Измените порядок.
  • У вас есть номер телефона, который начинается с + или содержит тире, где вы не ожидаете появления специальных символов? Удалите их.
  • Null collection — проблема? Убедитесь, что вы инициализируете это перед доступом (используя ленивую инициализацию или конструктор).

Не прерывайте выполнение кода из-за ошибок, которые вы можете исправить, и, конечно, не мешайте своими действиями пользователям своего сервиса или приложения. Если вы в состоянии понять проблему и решить ее на лету — просто сделайте это.



Возврат Null или других магических чисел


Нулевые значения, –1 там, где ожидается положительное число, и другие магические возвращаемые значения — все это порождение дьявола, которое переносит ответственность за проверку ошибок на вызывающую функцию.

С ними ваш код будет полон вот таких блоков, которые сделают неясной логику приложения:

return_value = possibly_return_a_magic_value()
if return_value < 0:
   handle_error()
else:
   do_something()
 
other_return_value = possibly_nullable_value()
if other_return_value is None:
   handle_null_value()
else:
   do_some_other_thing()

Ну или попроще, но тоже скверно:
var item = returnSomethingWhichCouldBeNull();
var result = item?.Property?.MaybeExists;
if (result.HasValue)
{
    DoSomething();
}

Передача null в методы также является проблемой, хотя в коде некоторых разработчиков вы можете встретить места, где методы начинаются с нескольких строк проверки правильности ввода. Но на самом деле все это не нужно. Большинство современных языков предоставляют не один, а сразу несколько инструментов, позволяющих четко указывать, чего вы ожидаете. Они же пропускают бесполезные проверки, например, определение параметров как ненулевых или обладающих соответствующим decorator.

Коды ошибок


Здесь та же проблема, что с null и другими подобными значениями, когда добавляется ненужное усложнение кода.

К примеру, можно использовать вот такую конструкцию для возврата кода ошибки:

int errorCode;
var result = getSomething(out errorCode);
if (errorCode != 0)
{
    doSomethingWithResult(result);
}

Результат можно выводить следующим образом:
public class Result<T>
{
   public T Item { get; set; }
   // At least "ErrorCode" is an enum
   public ErrorCode ErrorCode { get; set; } = ErrorCode.None;
   public IsError { get { return ErrorCode != ErrorCode.None; } }
}
 
public class UsingResultConstruct
{
   ...
   var result = GetResult();
   if (result.IsError)
   {
      switch (result.ErrorCode)
      {
         case ErrorCode.NetworkError:
             HandleNetworkError();
             break;
         case ErrorCode.UserError:
             HandleUserError();
             break;
         default:
             HandleUnknownError();
             break;
      }
   }
   ActuallyDoSomethingWithResult(result);
   ...
}

Это действительно не самый удачный код. Получается, что Item по-прежнему может быть пустым. Фактически, нет никакой гарантии (кроме соглашения), что, когда результат не содержит ошибки, вы можете безопасно получить доступ к Item.

После того как вы закончите со всей этой обработкой, вам все равно придется преобразовать код бага в сообщение об ошибке и что-то с ним сделать. Иногда случается так, что из-за излишнего усложнения кода уже само это сообщение выводится не так, как нужно.

Есть еще более серьезная проблема: если вы или кто-то другой измените внутреннюю реализацию для обработки нового недопустимого состояния с новым кодом ошибки, то вся конструкция вообще перестанет работать.

Если не получилось с первого раза, пробуйте еще


Прежде чем продолжить, стоит подчеркнуть, что «тихий» отказ программы — это плохо. Неожиданная проблема может возникнуть в самый неподходящий момент — например, на выходных, когда вы не можете быстро все исправить.



Если вы читали Clean Code, то, скорее всего, удивляетесь, почему просто не «бросить» исключение? Если нет, то скорее всего, вы считаете, что исключения — корень зла. Мне раньше тоже так казалось, но теперь я думаю немного иначе.
Интересный, по крайней мере для меня, нюанс состоит в том, что реализация по умолчанию для нового метода в C# заключается в создании исключения NotImplementedException, тогда как для нового метода в Python умолчанием является «pass».

В результате чаще всего мы вводим настройку «тихой ошибки» для Python. Интересно, сколько разработчиков потратили кучу времени для того, чтобы понять, что происходит и почему программа не работает. А в итоге через много часов обнаружили, что забыли реализовать метод заполнителя.
Но взгляните вот на это:

public MyDataObject UpdateSomething(MyDataObject toUpdate)
{
    if (_dbConnection == null)
    {
         throw new DbConnectionError();
    }
    try
    {
        var newVersion = _dbConnection.Update(toUpdate);
        if (newVersion == null)
        {
            return null;
        }
        MyDataObject result = new MyDataObject(newVersion);
        return result;
     }
     catch (DbConnectionClosedException dbcc)
     {
         throw new DbConnectionError();
     }
     catch (MyDataObjectUnhappyException dou)
     {
         throw new MalformedDataException();
     }
     catch (Exception ex)
     {
         throw new UnknownErrorException();
     }
}

Конечно, исключения сами по себе не защитят вас от создания нечитаемого и неуправляемого кода. Вы должны применять исключения как хорошо продуманную и выверенную стратегию. В противном случае, если масштаб проекта слишком велик, ваше приложение может оказаться в несогласованном состоянии. И наоборот, если проект слишком мал, вы получите хаос.

Для того чтобы этого не случилось, я советую иметь в виду вот что:

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

def my_function():
    try:
        do_this()
        do_that()
    except:
        something_bad_happened()
    finally:
        cleanup_resource()

Консолидация ошибок необходима. Хорошо, если вы предусматриваете различные типы обработки разных видов ошибок. Тем не менее это нужно вам, а не пользователям. Для них выводите единственное исключение, чтобы пользователи просто знали, что что-то пошло не так. Детали — ваша зона ответственности.

public MyDataObject UpdateSomething(MyDataObject toUpdate)
{
    try
    {       
        var newVersion = _dbConnection.Update(toUpdate);
        MyDataObject result = new MyDataObject(newVersion);
        return result;
     }
     catch (DbConnectionClosedException dbcc)
     {
         HandleDbConnectionClosed();
         throw new UpdateMyDataObjectException();
     }
     catch (MyDataObjectUnhappyException dou)
     {
         RollbackVersion();
         throw new UpdateMyDataObjectException();
     }
     catch (Exception ex)
     {
         throw new UpdateMyDataObjectException();
     }
}

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

def my_api():
    try:
        item = get_something_from_the_db()
        new_version = do_something_to_item(item)
        return new_version
    except Exception as ex:
        handle_high_level_exception(ex)

Пока это все, а если вы хотите обсудить тему ошибок и их обработки — велкам.

Skillbox рекомендует:

Комментарии (11)


  1. eefadeev
    21.02.2019 13:58

    Старое правило гласит: «Не перехватывайте сообщения об ошибках, которые вы не можете обработать».


    1. rstepanov
      21.02.2019 14:28

      Часто это что-то типа «не перехватывайте вообще никогда, техподдержка разберется».


      1. JustDont
        21.02.2019 14:39

        Тем не менее, это куда лучше варианта «перехватить, и ничего не сделать для урегулирования ошибки». Это и есть то самое «замалчивание», которое вредно во всех случаях без исключения.

        Не показывать ошибки юзеру — это нормально (если есть альтернативные методы их доставки до разработчиков, конечно), не показывать их вообще — это ужасно.


      1. eefadeev
        21.02.2019 15:49

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


    1. saipr
      22.02.2019 12:48

      Это в корне не верно


      1. eefadeev
        22.02.2019 13:15

        Редко приходится сталкиваться с аргументацией такой мощности и степени развёрнутости!


        1. saipr
          22.02.2019 15:02

          Краткость сестра таланта. Этот тот случай когда нужна тишина.


          1. eefadeev
            22.02.2019 15:32

            Лесть никогда не достигает цели если она обращена к самому себе.


            1. saipr
              22.02.2019 16:36

              Если к самому себЯлюбимому, то да не достигает.


  1. kzhyg
    22.02.2019 04:02

    > Консистентность превыше всего. Вам необходимо всегда убеждаться в том, что приложение согласовано.

    Проверяемые исключения спасут мир ;)


  1. saipr
    22.02.2019 12:47

    Как правильно обрабатывать ошибки: тишина — не всегда хорошо

    Какое шикарное название. Я бы даже сказал, что тишина при обработке ошибок это просто диверсия. Я всегда спрашиваю программисмтов/разработчиков ваша программа надежна? Мне отвечают, да она правильно все делает. А я им в ответ, я же спрашиваю про надежность а неправильность. Программист что-то может упустить или не так понять или задачу поставили неверно. Но программа должна работать и из любой ситуации находить выход из любой ситуации и сообщать пользователю о возникающих проблемах.
    Программист должен учить программу не молчать, а говорить.
    Еще раз за название спасибо.