如何编写好的异常消息


101

我当前正在进行代码审查,并且我注意到的一件事是,异常消息似乎只是在重申发生异常的地方的异常数量。例如

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

我可以从其余消息中得出此消息中的所有三个项目。我从堆栈跟踪中知道该类和方法,并且我知道它失败了(因为我有一个异常)。

这让我开始考虑将什么消息放入异常消息中。首先创建一个异常类,如果不存在,对于一般的原因(例如PropertyNotFoundException-的原因),然后当我把它的消息表示什么地方出了错(如“无法找到财产‘IDontExist’的节点1234 “- 什么)。哪里是StackTrace。该可以在日志中结束了(如果适用)。该如何是为开发者制定(并修复)

您还有其他引发异常的技巧吗?特别是关于创建新类型和异常消息。


4
这些是用于日志文件还是呈现给用户?
乔恩·霍普金斯

5
仅用于调试。它们可能最终以日志记录。它们不会显示给用户。我不喜欢向用户显示异常消息。
科林·麦凯

Answers:


70

我将把答案更多地指向异常之后发生的事情:这有什么好处,软件应该如何工作,用户应该如何处理异常?我在职业生涯初期遇到的一项很棒的技术是始终按以下三个部分报告问题和错误:上下文,问题和解决方案。使用此准则将极大地改变错误处理,并使软件大大改善,供操作员使用。

这里有几个例子。

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,并在无法充分描述出什么问题,具体原因的调用代码块中捕获了所有这些方法的潜在错误。上下文是以及如何解决它。这鼓励您编写更精细的错误,并在代码中的正确位置捕获并处理它们,以便您可以正确地阐明操作员应采取的步骤。

在一家公司中,我们拥有一流的运营商,他们非常了解该软件,并保留了自己的“运行手册”,从而丰富了我们的错误报告和建议的解决方案。为了认识到这一点,该软件开始在例外情况下包括指向运行手册的Wiki链接,以便提供基本的说明以及操作员随着时间推移可以进行更高级的讨论和观察的链接。

如果您有尝试该技术的准则,那么在创建自己的代码时应在代码中命名异常会变得更加明显。 NonRecoverableConfigurationReadFailedException成为您要向操作员更全面描述的内容的简写形式。我喜欢冗长,我认为这对于下一个接触我的代码以进行解释的开发人员来说会更容易。


1
+1这是一个很好的系统。哪个更重要:确保信息能够传达或使用简短的单词?
Michael K 2010年

3
我喜欢+1的解决方案包括,上下文,问题,解决方案
WebDev 2010年

1
这项技术非常有用。我肯定会利用它。
Kid Diamond

上下文是不必要的数据。它已经存在于堆栈跟踪中。有解决方案是可取的,但并非总是可行/有用的。大多数问题都是温和地停止应用程序或忽略挂起的操作,然后返回到应用程序主执行循环,希望下次您成功...异常类的名称应这样,以使解决方案变得显而易见,因为FileNotFound或者ConnectException您知道该怎么做))
gotnkoa

2
@ThomasFlinkow在您的示例跟踪中,堆栈跟踪中将包含init(),execute()和cleanup()。借助库中良好的命名架构和简洁/易于理解的API,您无需字符串解释。并且快速失败,不要在整个系统中都处于损坏状态。具有唯一ID的跟踪和日志记录可以解释应用程序的流程/状态。
Givenkoa

23

这个最近的问题中,我提出了一点,即异常根本不应该包含任何消息。我认为,他们这样做的事实是一个巨大的误解。我建议的是

异常的“消息”是异常的(完全限定)类名称

异常应在其自己的成员变量中包含尽可能多的有关所发生事件的详细信息。例如,一个IndexOutOfRangeException应该包含被发现无效的索引值,以及在引发异常时有效的上限值和下限值。这样,使用反射,您可以自动构造一条消息,内容如下:IndexOutOfRangeException: index = -1; min=0; max=5并且此消息以及堆栈跟踪信息应该是解决问题所需的所有客观信息。将其格式化为漂亮的消息(如“索引-1不在0到5之间”)不会添加任何值。

在您的特定示例中,NodePropertyNotFoundException该类将包含未找到的属性的名称,以及对不包含该属性的节点的引用。这一点很重要:它应该包含节点的名称; 它应该包含对实际节点的引用。在您的特定情况下,这可能不是必需的,但这是一个原则问题,是一种首选的思维方式:构造异常时,主要的考虑是必须由可能捕获该异常的代码来使用它。人类的可用性是一个重要的问题,但仅是次要的问题。

这可以解决您在职业生涯中某个时候可能遇到的令人沮丧的情况,在这种情况下,您可能会捕获到一个异常,该异常包含有关消息文本中发生的事情的重要信息,而不是有关其成员变量中发生的事情,因此您为了找出发生了什么,必须对文本进行字符串解析,希望消息文本在基础层的将来版本中保持不变,并祈祷当您的程序被使用时,消息文本不会使用某种外语。在其他国家/地区运行。

当然,由于异常的类名是异常的消息(而异常的成员变量是特定的详细信息),这意味着您需要很多不同的异常来传达所有不同的消息,并且那也行。

现在,有时候,在编写代码时,我们遇到一种错误的情况,在这种情况下,我们只想快速编写一条throw语句并继续编写代码,而不必中断我们正在做的事情来创建新的异常类,这样我们就可以把它扔在那里。对于这些情况,我有一个GenericException实际上接受字符串消息作为构造时参数的类,但是此异常类的构造函数装饰有一个很大的,巨大的亮紫色FIXME XXX TODO注释,指出该类的每个实例都必须是在发布软件系统之前,最好在提交代码之前,用一些更专门的异常类的实例化代替。


8
如果您使用的语言没有GC(例如C ++),则在将对任意数据的引用放入发送到堆栈的异常中时应格外小心。很有可能,在捕获异常之前,您引用的所有内容都已被销毁。
塞巴斯蒂安·雷德尔

4
@SebastianRedl是的。如果node对象由using-disposable(在C#中)或try-with-resources(在Java中)子句保护,则同样的情况也适用于C#和Java :带有异常的对象将被处置/关闭,从而使其成为非法。访问它以便在处理异常的地方从中获取有用的信息。我想在这种情况下,应该将某种对象的摘要存储在异常中,而不是对象本身。我想不出一种万无一失的方法来针对所有情况进行通用处理。
Mike Nakis

13

通常,异常应该通过提供有用的信息(期望值,实际值,可能的原因/解决方案等)来帮助开发人员查明原因

当没有任何内置类型有意义时,应创建新的异常类型。特定类型使其他开发人员可以捕获特定异常并进行处理。如果开发人员知道如何处理您的异常,但类型为Exception,则他将无法正确处理它。


+1-期望值与实际值非常有用。在问题给出的示例中,您不应该简单地说一个方法失败,而是要说它为什么失败(基本上是失败的确切命令以及导致失败的情况。)
Felix Dombek 2010年

4

在.NET中永远不会throw new Exception("...")(就像问题的作者所表明的那样)。Exception是根Exception类型,它不应该直接抛出。而是抛出一种派生的.NET异常类型,或创建自己的自定义异常(从异常(或另一种异常类型)派生)。

为什么不抛出异常?因为抛出Exception并不能描述您的异常,并且会迫使您的调用代码编写类似catch(Exception ex) { ... }这样的代码,但这通常不是一件好事!:-)。


2

您想要寻找的“添加”到异常的东西是那些不在异常或堆栈跟踪中的数据元素。这些是否是“消息”的一部分,或者在登录时是否需要附加是一个有趣的问题。

正如您已经指出的,该异常可能会告诉您什么,stacktrace可能会告诉您在哪里,但是“为什么”可能会涉及更多(应该是,有人希望),而不仅仅是看一两行然后说“ h!当然”。当在生产代码中记录错误时,情况更是如此-我经常被不良数据所困扰,这些不良数据已进入测试系统中不存在的实时系统。只需知道导致错误的数据库中记录的ID是什么,就可以节省大量时间。

所以...列出,或者,对于.NET,添加到记录的异常数据集合中(cf @Plip!):

  • 参数(这可能会有点有趣-如果它不会序列化,则有时不能将其添加到数据集合中,有时单个参数可能会非常复杂)
  • 由ADO.NET或Linq返回到SQL或类似的附加数据(这也可能会变得很有意思!)。
  • 还有什么可能不明显。

当然,有些事情直到您在初始错误报告/日志中没有它们时才知道您需要。在发现需要这些东西之前,您不会意识到自己可以获得的一些东西。


0

什么是例外

(1)告诉用户出了什么问题?

这应该是万不得已的方法,因为您的代码应该进行干预,并向他们展示比“异常”更“有趣”的东西。

“错误”消息应清楚简洁地指出出了什么问题以及用户可以采取哪些措施来从错误状态中恢复

例如“请不要再按此按钮”

(2)告诉开发人员何时出错?

这是您登录文件进行后续分析的一种方式。
堆栈跟踪会告诉开发者,其中的代码破坏; 该消息应再次指出出了什么问题。

(3)告诉异常处理程序(即代码)出了什么问题?

Exception 的类型将决定哪个异常处理程序将对其进行查看,并且在Exception对象上定义的属性将允许该处理程序对其进行处理。

异常的消息完全无关紧要


-3

如果可以,请不要创建新类型。它们可能引起额外的混乱,复杂性并导致需要维护的代码更多。它们可能使您的代码不得不扩展。设计异常层次结构需要进行大量的测试。这不是事后的想法。通常最好使用内置的语言异常层次结构。

异常消息的内容取决于消息的接收者-因此您必须将自己放在该人的鞋子上。

支持工程师将需要能够尽快识别错误的来源。包括简短的描述性字符串以及任何有助于解决问题的数据。始终包括堆栈跟踪(如果可以)-这将是唯一的真实信息源。

向系统的一般用户显示错误取决于错误的类型:如果用户可以解决问题(例如,通过提供不同的输入),则需要简洁的描述性消息。如果用户无法解决问题,则最好声明已发生错误并记录/发送错误以提供支持(使用上述准则)。

另外-不要抛出大的“ HALT ERROR!” 图标。这是一个错误-这不是世界末日。

因此,总而言之:考虑一下系统的参与者和用例。将自己放在这些用户的鞋子上。有所帮助。对人好点。在系统设计中先考虑一下这一点。从用户的角度来看,这些异常情况以及系统如何处理它们,与系统中的正常情况一样重要。


10
我不同意。当语言API不能满足您的确切需求时,有很多很好的理由来实现您自己的异常。一个原因是,在一种方法中,有很多事情可能会失败,您可以针对不同类型的异常编写不同的catch子句,从而可以对确切的问题做出反应。另一个是,您可以将代表不同抽象层的多个异常层分开,其中可以将异常所属的确切层编码为其类型。仅使用“ Exception”或“ IllegalStateException”和消息字符串对那里没有多大帮助。
Felix Dombek 2010年

1
我也不同意。异常类型对将使用它的调用代码有意义。例如,如果我正在调用框架,并且这在内部导致FileDoesNotExistException,那么作为框架的调用者,这对我来说可能没有任何意义。相反,可能最好创建一个自定义异常并将传入的异常作为内部异常传入。
bytedev '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.