Аргументы против подавления ошибок

35

Я нашел такой код в одном из наших проектов:

    SomeClass QueryServer(string args)
    {
        try
        {
            return SomeClass.Parse(_server.Query(args));
        }
        catch (Exception)
        {
            return null;
        }
    }

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

Когда уместно полностью подавить все ошибки, подобные этой?

user626528
источник
13
Эрик Липперт написал прекрасный пост в блоге под названием досадные исключения, в котором он классифицирует исключения по 4 различным категориям и предлагает правильный подход для каждой из них. Я думаю, что она написана с точки зрения C #, но применима ко всем языкам.
Damien_The_Unbeliever
3
Я считаю, что код нарушает принцип «Fail-fast». Существует высокая вероятность того, что в нем скрыта настоящая ошибка.
Euphoric
7
thecodelesscode.com/case/224
Дэн Нили,
4
Вы ловите много. Вы также будете ловить ошибки, такие как NRE, индекс массива за пределами границ, ошибки конфигурации ... Это мешает вам находить эти ошибки, и они будут постоянно наносить ущерб.
USR

Ответы:

52

Представьте код с тысячами файлов, используя несколько библиотек. Представьте, что все они закодированы так.

Представьте, например, что обновление вашего сервера приводит к исчезновению одного файла конфигурации; и теперь все, что у вас есть, это трассировка стека, исключение нулевого указателя, когда вы пытаетесь использовать этот класс: как бы вы решили это? Это может занять несколько часов, в течение которых, по крайней мере, простая регистрация трассировки необработанного стека файла, не найденного [путь к файлу], может позволить вам на мгновение разрешить проблему.

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

Даже без надежной обработки ошибок просто

throw new IllegalStateException("THIS SHOULD NOT HAPPENING")

или

LOGGER.error("[method name]/[arguments] should not be there")

может сэкономить вам часы времени.

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

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

Лучшие практики - это вещи, которым нужно следовать в большинстве случаев, может быть, 80%, может быть, 99%, но вы всегда найдете один крайний случай, когда они не применяются. В этом случае оставьте комментарий, почему вы не следуете практике для других (или даже для себя), которые будут читать ваш код несколько месяцев спустя.

Walfrat
источник
7
+1 за объяснение, почему вы думаете, что вам нужно сделать это в комментарии. Без объяснения, глотание исключений всегда вызывало бы тревогу в моей голове.
jpmc26
Моя проблема в том, что комментарий застрял внутри этого кода. Как пользователь функции, я никогда не смогу увидеть этот комментарий.
CorsiKa
@ jpmc26 Если исключение обрабатывается (например, путем возврата специального значения), это вовсе не исключение исключений.
Дедупликатор
2
@corsiKa потребитель не должен знать детали реализации, если функция выполняет свою работу должным образом. может быть, они что-то из этого в библиотеках Apache или Spring, и вы этого не знаете.
Вальфрат
@Deduplicator да, но использование улова как части вашего алгоритма не должно быть хорошей практикой :)
Walfrat
14

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

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

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

gbjbaanb
источник
2
«используется, когда сгенерированное исключение никогда не должно было быть» - ничего такого Смысл исключения состоит в том, чтобы сигнализировать о том, что что-то пошло не так.
Эйфорическая
3
@Euphoric Но иногда автор библиотеки не знает намерений конечного пользователя этой библиотеки. «Ужасно неправильный» у одного человека может быть чьим-то легко поддающимся восстановлению состоянием.
Саймон Б.
2
@ Euphoric, это зависит от того, что ваш язык считает «ужасно неправильным»: Python люди любят исключения для вещей, которые очень близки к управлению потоком в (например,) C ++. Возвращение нулевого значения может быть оправдано в некоторых ситуациях - например, чтение из кэша, где элементы могут быть внезапно и непредсказуемо выселены.
Мэтт Краузе
10
@Euphoric, «Файл не найден, не является нормальным исключением. Сначала вы должны проверить, существует ли файл, если есть вероятность, что он может не существовать». Нет! Нет! Нет! Нет! Это классическое неправильное мышление вокруг атомных операций. Файл может быть удален между вами, проверяя, существует ли он, и обращаясь к нему. Единственный правильный способ справиться с такими ситуациями - получить к нему доступ и обработать исключения, если они имеют место, например, вернувшись, nullесли это лучшее, что может предложить выбранный вами язык.
Дэвид Арно
3
@Euphoric: Итак, напишите код для обработки невозможности получить доступ к файлу хотя бы дважды? Это нарушает СУХОЙ, а также неиспользованный код - это неработающий код
Дедупликатор
7

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

   if(QueryServer(args)==null)
      TerminateProgram();

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

  result = QueryServer(args);
  if(result!=null)
      DoSomethingMeaningful(result);
  // else ??? Mask the error and let the user run into trouble later

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

Док Браун
источник
5

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

Две ситуации следующие:

1. Когда исключение не считалось исключительным состоянием

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

Некоторые необязательные атрибуты класса могут иметь в виду.

2. Когда вы предоставляете новую (лучше, быстрее?) Реализацию библиотеки с использованием интерфейса, уже используемого в приложении

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

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

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


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

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

Энди
источник
-1

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

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

По сути, правильная обработка исключения часто будет зависеть в меньшей степени от типа исключения, чем от места, где оно было сгенерировано; если ресурс защищен блокировкой чтения-записи, и в методе, который получил блокировку для чтения, генерируется исключение, обычно правильное поведение должно состоять в снятии блокировки, поскольку метод не может ничего сделать с ресурсом , Если во время получения блокировки для записи возникает исключение, блокировку часто следует аннулировать, поскольку защищенный ресурс может находиться в недопустимом состоянии; если у блокирующего примитива нет состояния «недействительно», следует добавить флаг для отслеживания такой аннулирования. Снятие блокировки без ее аннулирования - это плохо, потому что другой код может видеть защищенный объект в недопустимом состоянии. Однако, оставляя замок висящим, или правильное решение. Правильное решение состоит в том, чтобы аннулировать блокировку так, чтобы любые ожидающие или будущие попытки получения немедленно потерпели неудачу.

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

Supercat
источник
2
Это не дает ответа на вопрос, потому что обработка исключения без знания подробностей об исключении! = Тихое использование (общего) исключения.
Taemyr
@Taemyr: Если целью функции является «сделать X, если это возможно, или указать, что это не так», а X было невозможно, часто будет непрактично перечислять все виды исключений, которые ни одна из функций Кроме того, вызывающему абоненту не нужно заботиться о том, что «Икс сделать невозможно». Обработка исключений Pokemon часто является единственным практическим способом заставить вещи работать в таких случаях, и они не должны быть почти такими же злыми, как это делают некоторые люди, если дисциплинирован код о недействительности вещей, которые искажаются неожиданными исключениями.
суперкат
В точку. Каждый метод должен иметь цель, если он может полностью выполнить свою цель, независимо от того, вызывает ли какой-либо метод, который он вызывает, исключение или нет, тогда правильное решение - проглотить исключение, чем бы оно ни было, и выполнить свою работу. Обработка ошибок в основном заключается в завершении задачи после ошибки. Вот что важно, а не какая ошибка произошла.
Jmoreno
@jmoreno: Что усложняет ситуацию, так это то, что во многих ситуациях будет большое количество исключений, многие из которых программист не предвидел бы, что не указывало бы на проблему, и несколько исключений, некоторые из которых программист не хотел бы не особенно предвидеть, которые действительно указывают на проблему, и нет определенного систематического способа их различения. Единственный способ, с помощью которого я знаю, чтобы иметь дело с этим, - это исключать недействительные вещи, которые были испорчены, поэтому, если они имеют значение, их замечают, а если они не имеют значения, их игнорируют.
суперкат
-2

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

Тогда что они делают? Они добавят код для обработки исключения, чтобы увидеть, какой вариант исключения они получают. Отлично. Но если обработчик исключений остается, исходный вызывающий объект больше не возвращается в ноль, и что-то ломается в другом месте.

Даже если вы знаете, что какой-то вышестоящий код может выдать ошибку и, таким образом, вернуть значение null, с вашей стороны было бы серьезно инертно не пытаться, по крайней мере, попытаться предотвратить исключение в вызывающем коде IMHO.

Робби Ди
источник
«серьезно инертный» - возможно, вы имели в виду «серьезно неумелый»?
Эзотерическое имя
@EsotericScreenName Pick one ... ;-)
Робби Ди
-3

Я видел примеры, когда сторонние библиотеки имели потенциально полезные методы, за исключением того, что в некоторых случаях они генерируют исключения, которые должны работать для меня. Что я могу сделать?

  • Реализуйте именно то, что мне нужно. Может быть сложно в срок.
  • Изменить сторонний код. Но тогда я должен искать конфликты слияния с каждым обновлением.
  • Напишите метод-обертку, чтобы превратить исключение в обычный нулевой указатель, логическое значение false или что-то еще.

Например, метод библиотеки

public foo findFoo() {...} 

возвращает первый foo, но если его нет, он генерирует исключение. Но мне нужен метод возврата первого foo или null. Итак, я пишу это:

public foo myFindFoo() {
    try {
        return findFoo() 
    } 
    catch (NoFooException ex) {
        return null;
    }
}

Не аккуратно. Но иногда прагматичное решение.

ом
источник
1
Что вы имеете в виду «выбросить исключения, где они должны работать для вас»?
CorsiKa
@corsiKa, пример, который мне приходит в голову, это методы поиска по списку или схожей структуре данных. Они терпят неудачу, когда они ничего не находят, или они возвращают нулевое значение? Другим примером являются методы waitUntil, которые по истечении времени ожидания дают сбой, а не возвращают логическое значение false.
ом