Как написать хорошее сообщение об исключении

101

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

throw new Exception("BulletListControl: CreateChildControls failed.");

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

Это заставило меня задуматься о том, какое сообщение я помещаю в сообщения об исключениях. Сначала я создаю класс исключения, если один уже не существует, по общей причине (например PropertyNotFoundException- почему ), а затем , когда я бросаю его сообщение указывает на то , что пошло не так (например , «Не удалось найти свойство„IDontExist“на узле 1234 "- то, что ). Где находится в StackTrace. , Когда может закончиться в журнале (если применимо). Как для разработчика , чтобы работать (и исправить)

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

Колин Маккей
источник
4
Это для файлов журналов или для представления пользователю?
Джон Хопкинс
5
Только для отладки. Они могут оказаться в журнале. Они не будут представлены пользователю. Я не фанат представления сообщений об исключениях пользователю.
Колин Маккей

Ответы:

72

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

Вот несколько примеров.

Context: Saving connection pooling configuration changes to disk.
Problem: Write permission denied on file '/xxx/yyy'.
Solution: Grant write permission to the file.

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

Context: Sending email to 'abc@xyz.com' regarding 'Blah'.
Problem: SMTP connection refused by server 'mail.xyz.com'.
Solution: Contact the mail server administrator to report a service problem.  The email will be sent later. You may want to tell 'abc@xyz.com' about this problem.

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

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

Вторая замечательная вещь, которая приходит на ум, - это когда вы пытаетесь описать решение в своем исключении и пишете «Проверьте X, и если A, то B, еще C». Это очень четкий и очевидный признак того, что ваше исключение проверяется не в том месте. У вас, программиста, есть возможность сравнивать вещи в коде, поэтому «если» операторы должны выполняться в коде, зачем вовлекать пользователя в то, что можно автоматизировать? Скорее всего, это из-за более глубокого в коде, и кто-то сделал ленивую вещь и выбросил IOException из любого числа методов и уловил потенциальные ошибки от всех из них в блоке вызывающего кода, который не может адекватно описать, что пошло не так, что конкретноеконтекст есть и как это исправить. Это побуждает вас писать более мелкие ошибки, отслеживать и обрабатывать их в нужном месте вашего кода, чтобы вы могли правильно сформулировать шаги, которые должен предпринять оператор.

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

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

Сэр вобин
источник
1
+1 Это хорошая система. Что важнее: быть уверенным, что информация передается, или использовать короткие слова?
Майкл К
3
+1, потому что мне нравится решение, включающее контекст, проблему, решение
WebDev
1
Эта техника очень полезна. Я определенно собираюсь использовать это.
Малыш Даймонд
Контекст ненужных данных. Это уже присутствует в трассировке стека. Наличие решения предпочтительнее, но не всегда возможно / полезно. Большинство проблем - это своего рода изящная остановка приложения или игнорирование отложенных операций и возврат к главному циклу выполнения приложения, надеясь, что в следующий раз вы добьетесь успеха ... Имя класса исключения должно быть таким, чтобы решение стало очевидным, FileNotFoundили ConnectExceptionвы знаете, что делать))
gavenkoa
2
@ThomasFlinkow В вашем примере трассировки будут иметь init (), execute () и cleanup () в трассировке стека. С хорошей схемой именования в библиотеке и чистым / понятным API вам не нужны строковые объяснения. И быстро проваливайтесь, не несите нарушенное состояние по всей системе. Трассировки и записи в журнале с уникальным идентификатором могут объяснить поток / состояние приложения.
gavenkoa
23

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

«Сообщение» об исключении - это (полностью определенное) имя класса исключения.

Исключение должно содержать в своих переменных-членах как можно больше подробностей о том, что именно произошло; например, IndexOutOfRangeExceptionдолжен содержать значение индекса, который был признан недействительным, а также верхнее и нижнее значения, которые действовали в момент, когда было сгенерировано исключение. Таким образом, используя отражение, вы можете автоматически создать сообщение, которое будет выглядеть следующим образом: IndexOutOfRangeException: index = -1; min=0; max=5и вместе с трассировкой стека это должна быть вся объективная информация, необходимая для устранения проблемы. Форматирование его в красивое сообщение типа «индекс -1 не было между 0 и 5» не добавляет никакого значения.

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

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

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

Теперь, иногда, когда мы пишем код, мы сталкиваемся с ошибочной ситуацией, для которой мы просто хотим быстро кодировать throwинструкцию и продолжать писать наш код вместо того, чтобы прерывать то, что мы делаем, чтобы создать новый класс исключений, чтобы мы могли брось это прямо там. Для этих случаев у меня есть GenericExceptionкласс, который на самом деле принимает строковое сообщение в качестве параметра времени создания, но конструктор этого класса исключений украшен огромным огромным ярко-фиолетовым FIXME XXX TODOкомментарием, утверждающим, что каждый экземпляр этого класса должен быть заменено созданием некоторого более специализированного класса исключений перед выпуском системы программного обеспечения, предпочтительно перед фиксацией кода.

Майк Накис
источник
8
Если вы находитесь на языке, который не имеет GC, например C ++, вы должны быть очень осторожны при размещении ссылок на произвольные данные в исключениях, которые вы отправляете в стек. Скорее всего, все, на что вы ссылаетесь, было уничтожено к тому времени, когда было обнаружено исключение.
Себастьян Редл
4
@SebastianRedl Правда. И то же самое может быть применимо к C # и Java, если nodeобъект защищен предложением using-disable (в C #) или try-with-resources (в Java): объект, сохраненный с исключением, будет удален / закрыт, что сделает его недопустимым получить доступ к нему, чтобы получить любую полезную информацию из него в месте обработки исключения. Я полагаю, что в этих случаях некоторая сводка объекта должна храниться в исключении, а не в самом объекте. Я не могу думать о безошибочном способе универсально обращаться с этим для всех случаев.
Майк Накис
13

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

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

mbillard
источник
+1 - ожидаемые значения против фактических значений чрезвычайно полезны. В примере, приведенном в вопросе, вы должны не просто сказать, что метод потерпел неудачу, но и почему он потерпел неудачу (в основном, точная команда, которая потерпела неудачу, и обстоятельства, которые вызвали ошибку.)
Феликс Домбек
4

В .NET никогда не throw new Exception("...")бывает (как показал автор вопроса). Исключение является корневым типом исключения и, как предполагается, никогда не генерируется напрямую. Вместо этого бросьте один из производных типов исключений .NET или создайте свое собственное пользовательское исключение, которое является производным от исключения (или другого типа исключения).

Почему бы не бросить исключение? Потому что создание Exception ничего не делает для описания вашего исключения и вынуждает ваш вызывающий код писать код, catch(Exception ex) { ... }который, как правило, не очень хорошая вещь! :-).

bytedev
источник
2

То, что вы хотите найти, чтобы «добавить» исключение, это те элементы данных, которые не присущи исключению или трассировке стека. Являются ли они частью «сообщения» или должны быть прикреплены при регистрации - интересный вопрос.

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

Итак ... Либо перечислены, либо, для .NET, добавлены в сбор данных зарегистрированных исключений (cf @Plip!):

  • Параметры (это может быть немного интересно - вы не можете добавить к коллекции данных, если она не сериализуется, а иногда один параметр может быть удивительно сложным)
  • Дополнительные данные, возвращаемые ADO.NET или Linq в SQL или аналогичные (это также может быть немного интересным!).
  • Все остальное может не быть очевидным.

Некоторые вещи, конечно, вы не будете знать, что вам нужно, пока их нет в вашем первоначальном отчете / журнале ошибок. Некоторые вещи, которые вы не понимаете, вы можете получить, пока не обнаружите, что они вам нужны.

Murph
источник
0

Какие исключения для ?

(1) Сказать пользователю что-то пошло не так?

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

Сообщение об ошибке должно четко и кратко содержать информацию о том, что пошло не так и что, если что-либо, пользователь может сделать для восстановления после ошибки.

например, «Пожалуйста, не нажимайте эту кнопку снова»

(2) Сообщать разработчику, когда что-то пошло не так?

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

(3) Сообщать обработчику исключений (т.е. коду), что что-то пошло не так?

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

Сообщение Исключения совершенно не имеет значения .

Фил В.
источник
-3

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

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

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

Представление ошибок обычным пользователям вашей системы зависит от типа ошибки: если пользователь может решить проблему, например, предоставив различные входные данные, требуется краткое описательное сообщение. Если пользователь не может решить проблему, лучше указать, что произошла ошибка, и зарегистрировать / отправить ошибку в службу поддержки (используя приведенные выше рекомендации).

Кроме того - не бросайте большую «ОШИБКУ ХАЛТА!» икона. Это ошибка - это не конец света.

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

Конор
источник
10
Я не согласен. Есть много веских причин для реализации ваших собственных исключений, когда языковой API не покрывает ваши конкретные потребности. Одна из причин заключается в том, что в методе, где несколько вещей могут потерпеть неудачу, вы можете написать разные предложения catch для разных типов исключений, где вы можете реагировать на конкретную проблему. Другое состоит в том, что вы можете разделить несколько уровней исключений, представляющих разные уровни абстракции, где точный уровень, к которому принадлежит исключение, может быть закодирован в его типе. Просто использование «Exception» или «IllegalStateException» и строка сообщения не сильно помогают там.
Феликс Домбек
1
Я тоже не согласен. Типы исключений должны иметь смысл для вызывающего кода, который будет его использовать. Например, если я вызываю фреймворк, и это вызывает внутреннее исключение FileDoesNotExistException, то для меня, как для вызывающего фреймворка, это может не иметь никакого смысла. Вместо этого может быть лучше создать пользовательское исключение и передать выброшенное исключение в качестве внутреннего исключения.
bytedev 10.10.16