Pull to refresh
0
Skillbox
Образовательная платформа Skillbox

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

Reading time5 min
Views6.3K
Original author: Rina Artstain


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

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

Напоминаем: для всех читателей «Хабра» — скидка 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 рекомендует:

Tags:
Hubs:
Total votes 32: ↑27 and ↓5+22
Comments11

Articles

Information

Website
skillbox.ru
Registered
Founded
Employees
501–1,000 employees
Location
Россия