为什么系统组件中的许多常见异常不包含有用的细节?
几个示例:
.NET
List
索引访问ArgumentOutOfRangeException
不会告诉我尝试过且无效的索引值,也不会告诉我允许的范围。基本上,来自MSVC C ++标准库的所有异常消息都是完全无用的( .NET中的Oracle异常,告诉您(找不到)“找不到表或视图”,但没有告诉您。
所以,在我看来大多数情况下,异常消息没有包含足够有用的详细信息。我的期望与众不同吗?我是否使用异常错误而我甚至注意到了这一点?还是我的印象是错误的:大多数例外确实提供了有用的细节?
#1 楼
异常不包含有用的细节,因为异常的概念在软件工程领域内还不够成熟,因此许多程序员对它们的理解并不充分,因此他们没有适当地对待它们。是的,
IndexOutOfRangeException
应该包含超出范围的精确索引,以及在抛出该索引时有效的范围,对于.NET运行时的创建者而言,它不是可鄙的。是的,Oracle的table or view not found
异常应包含未找到的表或视图的名称,同样,对于负责此操作的人来说,它也不可轻视。在某种程度上,这种混淆源于误导性的原始观念,即异常应该包含人类可读的消息,这又是由于对异常的含义缺乏了解,因此这是一个恶性循环。
由于人们认为异常应包含人类可读的消息,因此他们认为异常所携带的任何信息也应格式化为人类可读的消息,然后他们要么无聊到编写所有人类可读的消息构建代码,否则他们担心这样做会将不明智的信息泄露给任何窥探者可能看到的消息。 (其他答案提到了安全性问题。)但是,事实是,他们不必为此担心,因为该异常不应包含人类可读的消息。异常是只有程序员才能看到和/或处理的东西。如果需要将故障信息提供给用户,则必须以非常高的级别,以一种复杂的方式并以用户的语言来进行操作,从统计上讲,这不太可能是英语。
因此,对于我们程序员而言,异常的“消息”是异常的类名,并且与该异常有关的任何其他信息都应复制到异常对象的(最终/只读)成员变量中。优选地,每个可能的一点点。这样,不需要(或不应该)生成任何消息,因此没有窥视的眼睛可以看到它。
要解决托马斯·欧文斯在下面的评论中表达的关注:
是的,当然,在某种程度上,您将创建有关该异常的日志消息。但是您已经知道您在说什么了:一方面,没有堆栈跟踪的异常日志消息是没有用的,但是另一方面,您不想让用户看到整个异常堆栈跟踪。同样,这里的问题是我们的观点受到传统做法的歪曲。日志文件传统上是纯文本格式,在我们刚起步时可能还不错,但也许不再适用:如果出于安全考虑,则日志文件必须是二进制和/或加密的。 >
无论是二进制文本还是纯文本,都应将日志文件视为应用程序将调试信息序列化到的流。这样的流仅供程序员使用,并且为异常生成调试信息的任务应与将异常序列化到调试日志流中一样简单。这样,通过查看日志,您可以看到异常类名称(正如我已经说过的,它实际上是“消息”,)每个异常成员变量都描述了与以下内容相关的所有内容:并在实际中包含日志,以及整个堆栈跟踪。请注意,此过程中如何明显缺少可读格式的异常消息。
PS
我在此问题上还有其他一些想法:如何编写良好的异常消息
PPS
我对二进制日志文件的建议似乎使很多人被打勾,因此我再次修改了答案,以使我更加清楚地表明我的建议不是日志文件应为二进制文件,但如果需要,则日志文件可以为二进制文件。
评论
我查看了.NET Framework中的某些异常类,事实证明,有很多机会可以通过编程方式添加此类信息。所以我想这个问题可以解决为“为什么不呢”。但对于整个“人类可读”的事物,则为+1。
–罗伯特·哈维(Robert Harvey)
2015年4月13日在16:18
我不同意例外不应包含人类可读的组件。在某种程度上,您可能想要创建有关异常的日志消息。我认为将堆栈跟踪记录到用户可读的日志文件中会暴露您不想公开的实现细节,因此应该记录人类可读的消息。当出现包含错误的日志文件时,开发人员应该有一个起点来开始调试并能够强制发生异常。易于阅读的组件应适当详细地描述,而无需放弃实现。
–托马斯·欧文斯♦
15年4月13日在16:20
程序员不是人类吗?看着我的同事,这证实了我一段时间以来的一些怀疑...
– gbjbaanb
15年4月13日在16:22
同样,只要软件是客户端,让用户看到整个堆栈跟踪就没有错。我曾经从事的每个专业软件项目,以及大多数业余软件项目,都包含一个日志记录系统,当引发未处理的异常时,该系统将生成完整的错误转储,包括对当前所有正在运行的线程的完整堆栈跟踪处理。用户可以(惊天动地,恐怖!)随时查看它,因为这是必要的(不仅有用,而且是必需的),以便将错误消息发回给我们!怎么了
–梅森·惠勒
15年4月13日在18:31
对于纯二进制日志文件,我也不满意。主要是因为我在systemd方面的经验。它用于查看这些日志的特殊工具非常令人困惑,并且似乎是由莎士比亚的猴子委员会设计的。考虑到,对于一个Web应用程序,第一个看到您的异常的人通常是sysadmin,他将要确定是否需要修复(例如磁盘空间不足)或回传给他。给开发商。
–迈克尔·汉普顿
2015年4月14日在4:52
#2 楼
为什么系统组件中的许多常见异常不包含有用的详细信息?
根据我的经验,有很多原因导致异常不包含有用的信息。我希望这些原因也适用于系统组件-但我不确定。
注重安全的人员将异常视为信息泄漏的来源(例如) 。由于异常的默认行为是向用户显示该信息,因此程序员有时会谨慎行事。
在C ++中,我听说过有人反对在catch块中分配内存(至少在某些上下文中)发出好消息的谎言)。这种分配很难管理,而且更糟的是,可能导致该处的内存不足异常-经常使您的应用程序崩溃或内存泄漏。不分配内存就很难很好地格式化异常,而且这种习惯可能像程序员一样在各种语言之间迁移。
他们不知道。我的意思是,在某些情况下,代码不知道出了什么问题。如果代码不知道-它不会告诉你。
我在一些地方化的问题下无法将仅英语字符串放入系统中-即使是只能由英语支持人员阅读的例外情况员工。
在某些地方,我已经看到异常的使用更像是断言。他们在开发期间向您发出清晰,响亮的信息,表示某件事未完成,或者已在一个地方进行了更改,但在另一个地方没有进行更改。这些通常是足够独特的,以至于好消息将被重复使用或使人迷惑。
人们很懒惰,程序员比大多数人都要懒。我们在例外道路上花费的时间比幸福道路少得多,这是副作用。
我的期望与众不同吗?我在使用异常时是否注意到了这个错误?
金田我的意思是,例外应该有很好的信息,但它们也是例外。您应该花时间设计代码以避免出现异常情况,或者编写代码来处理异常情况(忽略消息),而不是在编码时将它们用作一种交互式反馈机制。不可避免地将它们用于调试目的,但在大多数情况下应将其保持在最低水平。您注意到此问题使我担心您在阻止它们方面做得不够好。
评论
我知道它已被标记为C,但是我要补充一点,您的最后一段并不适用于所有语言,因为某些语言可能(正确或错误)严重依赖基于异常的错误处理和报告。
– Liilienthal
15年4月13日在17:36
@Lilienthal-如?我不熟悉的语言不会定期执行此类操作。
– Telastyn
15年4月13日在17:48
我认为这个答案有很多很好的内容,但它避免了说“他们应该”的底线。
–djechlin
15年4月13日在18:55
谢谢。您的担心毫无根据(我希望:-)。但是每次我花一分钟的时间来查找该单元测试中出了什么问题或花更多的时间来分析代码(因为日志文件缺少信息)时,我都会为某些消息的可避免性而烦恼:-)
–马丁·巴
15年4月13日在19:24
@Telastyn SAP的专有ABAP具有异常类构造,可以包含一条消息,该消息基本上是专门用于向用户报告程序状态(成功,失败,警告)的对象以及动态(多语言)消息。我承认我不知道这种事情有多广泛,或者是否有可能在各种语言中得到鼓励,但至少有一种(令人遗憾的)普遍做法。
– Liilienthal
15年4月13日在20:54
#3 楼
我没有过多的C#经验,也没有专门的C ++,但是我可以告诉您-开发人员编写的异常(十分之九)比您所能找到的任何通用异常都有用。理想上是,一个通用异常将向您指出错误发生的确切原因,并且您可以轻松修复该错误-但实际上,在具有多个类且可以抛出各种各样变化的大型应用程序中对于不同种类的异常或相同种类的异常,编写自己的输出以返回错误总是比依赖默认消息更有价值。
应该是这样,因为正如许多人指出的那样,出于安全原因或为了安全起见,某些应用程序不想抛出不希望用户看到的错误消息。避免混淆他们。
相反,您应该在设计中预见应用程序中可能会引发哪些类型的错误(并且总会出现错误),并编写错误捕获消息来帮助您识别问题。
这并不总能为您提供帮助-因为您无法始终预期出什么错误消息将是有用的-但从长远来看,这是更好地了解自己的应用程序的第一步。
#4 楼
问题是专门问为什么“系统组件”(又称标准库类)引发的这么多异常不包含有用的详细信息。不幸的是,大多数开发人员没有在标准库中编写核心组件,也没有是必须公开的详细设计文件或其他设计原理。换句话说,我们可能永远无法确定。但是,有两个关键点需要牢记,为什么详细的异常信息可能不理想或不重要:
可以通过任何方式调用代码来使用异常:标准库无法对使用异常的方式施加约束。具体地,可以将其显示给用户。考虑数组索引超出范围:这可能为攻击者提供有用的信息。语言设计人员不知道应用程序将如何使用抛出的异常,甚至不知道它是哪种类型的应用程序(例如Web应用程序或桌面应用程序),因此从安全的角度来看,遗漏信息可能更安全。
显示给用户。而是显示友好的错误消息,并将异常记录到攻击者无法访问的位置(如果适用)。一旦发现错误,开发人员应调试代码,检查堆栈框架和逻辑路径。在这一点上,开发人员拥有比异常所希望拥有的更多的信息。
评论
1.根据此标准,似乎相对任意地显示了什么信息(“索引超出范围”,堆栈跟踪)和未显示(索引值)。 2.当已知相关的动态值时,调试可能会更快,更容易。例如,它通常会立即告诉您问题是出在错误的代码段上的垃圾输入,还是代码无法正确处理良好的输入
–本·亚伦森
2015年4月13日15:58
@BenAaronson异常的标识/类告诉我们错误的类型。我的观点是,出于安全考虑,可能会省略详细信息(即,导致错误的特定值)。该值可以追溯到用户输入,从而向攻击者显示信息。
–user22815
2015年4月13日在16:00
@Snowman我几乎不认为当有完整的堆栈跟踪可用而索引号没有可用时,安全性不是考虑因素。当然,我理解攻击者正在探查缓冲区溢出,但是许多异常也忽略了相当安全的数据(例如,未找到哪个Oracle表)
– gbjbaanb
15年4月13日在16:33
@gbjbaanb谁说我们需要向用户显示完整的堆栈跟踪信息?
–user22815
15年4月13日在16:37
感谢您分享这一见解。就我个人而言,我不同意并且认为安全性论点是完全谬误,但可能是合理的。
–马丁·巴
15年4月13日在19:38
#5 楼
首先,让我破灭一个泡沫,说即使在诊断消息中加载了在4秒内将您带到确切的代码行和子命令的信息的情况,用户也有可能永远不会将其写下或传达给支持人员然后您会被告知“它说了一些违规行为……我不知道它看起来很复杂!”我一直在编写软件并为其他软件的结果提供支持30年来,个人而言,当前的异常消息质量实际上与安全性无关,无论最终结果是如何适应其宇宙模型的,这与我们中有如此众多的事实有关行业本来是自学成才的,但他们从来没有在交流中上过课。也许如果我们将所有新编码器强制为维护职位几年,而他们不得不处理找出错误的地方,他们会理解至少某种形式的精度的重要性。
最近在正在重建的应用程序中,决定将返回码归为以下三个组之一:
0到9将通过附加的
信息获得成功。
10到99将是非致命性(可恢复)
错误,而
101到255将是致命性错误。
(100由于某种原因被遗漏了)
在任何特定的工作流程中,我们的思想都被重用了,否则通用代码将使用通用返回值(> 199)来处理致命错误,使我们在工作流程中可能出现100个致命错误。消息中的数据略有差异,找不到文件之类的错误都可以使用相同的代码并与消息进行区分。
当代码从承包商返回时,您不会相信我们会感到惊讶实际上,每个致命错误都是返回码101。
考虑到所有这些,我认为您的问题的答案是消息是如此无意义,因为最初创建消息时,它们是占位符,没人回过头。最终,人们找到了解决问题的方法,而不是因为出现了消息,而是发现了问题。
自那时以来,自学成才的人们从来没有一个很好的例子来说明异常应该包含什么。除此之外,更多的用户不阅读消息,更少的尝试将其传递给支持(我看到过的错误消息是由使用过的用户剪切并粘贴的,然后经过删除后再发送,并带有稍后的注释。似乎是很多信息,我可能并不需要全部,所以他们随机删除了一堆。
让我们面对它,因为其中有太多(不是全部,而是太多)下一代编码器,如果更多的工作并且不添加闪存,那就不值得了...
最后一点:如果错误消息中包含错误/返回码,在我看来在执行的模块中的某处应该有一行,内容如“ if condition return code-value”,而condition应该告诉您为什么会出现返回代码。这似乎是一种简单的逻辑方法,但对我而言,只是尝试让Microsoft告诉您,当Windows升级失败时对CODE 80241013或其他一些非常独特的标识符发生了什么。是吗?
评论
“ ...几乎每一个致命错误都返回代码101时,您不会相信我们会感到惊讶。”你是对的。我不会相信承包商对您的指示感到惊讶。
–奥达里克
15年4月15日在11:55
可能是用户永远不会将其写下来……而您将被告知“它说了一些违规信息……”这就是为什么您使用异常记录工具自动生成包含堆栈跟踪以及可能的错误报告的原因甚至将其发送到您的服务器。我曾经有一个用户不是很熟练。每次她从错误记录器提交一条消息时,都会出现类似“我不确定自己做错了什么,但是...”之类的事情,无论我解释了多少次错误表明该错误就在我身边。 。但是,我总是收到她的错误报告!
–梅森·惠勒
2015年4月28日在21:21
#6 楼
虽然我确实同意,例外应该包含尽可能多的信息,或者至少应包含较少的通用信息。在“找不到表”的情况下,表名会很好。但是您在收到异常的代码中更多地了解了要执行的操作。当您无法控制的库中发生错误时,您通常通常通常并不能采取很多措施来纠正这种情况,但是您可以添加更多有用的信息,以了解在什么情况下出现了错误。
找不到表的情况下,如果被告知无法找到的表称为STUDENTS,这对您没有多大帮助,因为您没有这样的表,并且该字符串在您的代码中不存在。
但是,如果您捕获了异常并使用尝试执行的SQL语句将其重新抛出,则情况会更好,因为事实证明您试图插入名称字段为Robert')的记录;拖放表学生; (总是有一个xkcd!)
因此,要克服信息量少的异常:try-catch-rethrow,其中包含有关您要执行的操作的更多信息。
我可能应该补充一点,因为这是关于问题原因的更多答案,原因是图书馆制造者关注的焦点不是一直在使异常消息更好,而他们却不知道为什么尝试了失败的东西,该逻辑在调用代码中。
评论
例如在C ++中,应该经常使用throw_with_nested。
–Miles Rout
15年4月14日在23:01
#7 楼
给出一个稍微不同的答案:可能已经对spec进行了破坏性代码:该函数接收X并返回Y
如果X无效,则抛出异常Z
增加了在最短的时间内以最小的麻烦准确交付规格的压力(以免在审查/测试中被拒绝),然后您便制定了一个完全合规且无济于事的库例外的食谱。 >
#8 楼
异常具有特定于语言和实现的成本。例如,需要C ++异常来销毁在抛出调用帧和捕获调用帧之间的所有活动数据,这很昂贵。因此,程序员不希望大量使用异常。
在Ocaml中,异常抛出的速度几乎与C
setjmp
一样快(其代价不取决于遍历的调用帧的数量),因此开发人员可以大量使用它(即使是非异常情况,也很常见)。相比之下,C ++异常足够繁琐,因此您可能不会像在Ocaml中那样大量使用它们。一个典型的示例是一些递归搜索或探索,可以在相当多的内部“停止”深度递归(例如,在树中找到叶子或统一函数)。在某些语言中,将这种条件传播给每个呼叫者的速度更快(因此更为惯用)。在其他语言中,抛出异常的速度更快。
因此,根据语言(以及开发人员使用它的习惯)的不同,异常可能包含许多有用的细节,或者相反地,快速的非本地跳转,仅携带非常有用的数据。
评论
“例如,C ++异常需要销毁在抛出调用框架和捕获调用框架之间的所有活动数据,而且这很昂贵。因此,程序员不希望过多使用异常。”总废话。 C ++异常的主要优点是确定性调用了析构函数。如果他们跳了,没人会用它们。
–Miles Rout
15年4月14日在22:57
我知道,但这是事实,C ++异常与Ocaml不同,并且您不会像在Ocaml中那样使用它们。
–Basile Starynkevitch
2015年4月15日在4:18
请勿将C ++异常用于C ++中的控制流,这主要是因为这样做会使代码难以阅读且难以理解。
–Miles Rout
15年4月15日在5:11
#9 楼
TL; DR:没有代码可以询问异常的Message属性。为什么许多异常消息不包含有用的详细信息?
可以说,因为它们都不应该!
/>异常对象应携带尽可能多的信息,这些信息对于异常处理程序完成其工作即“处理”异常并使之“消失”。
异常的类,在某些技术中,其属性可用于有选择地捕获要处理的异常。为什么没有这样的机制可以与异常消息一起使用?因为结构化异常处理体系结构的设计者意识到了这一点,所以简单的事实...
异常消息在很大程度上是多余的文档,用于人员而非程序。
异常消息在那里主要用于应用程序的“后退”异常处理程序,该处理程序位于调用堆栈的顶部,可捕获异常并在应用程序崩溃和刻录之前将其注销以进行诊断。
日志文件中的标题错误消息可以很容易地发现。
异常消息还具有一种讨厌的习惯,即被认为现有措辞是正确的开发人员进行编辑,重新措词,大约随机播放或翻译笨拙,无意义,过于冗长,技术上不准确或其他101个原因,当他们这样做时,任何希望找到嵌入消息中的“有用”内容的特定“位”的代码都将突然停止工作。
评论
然后,与其对我为什么认为自己完全不合时宜发表冗长的评论,我想我将对此发表自己的想法:stackoverflow.com/a/27825133/321013
–马丁·巴
10月9日18:37
我刚刚注意到“ TL; DR:任何代码都不应询问异常的Message属性。”我完全同意您的TL; DR。但这完全没有意义,因为除了将其转储到日志之外,没有代码会检查Message。
–马丁·巴
10月12日20:04
#10 楼
是什么让您认为索引值或所需的范围或表的名称是一个有用的细节(对于异常)?异常不是错误处理机制;它不是错误处理机制。它们是一种恢复机制。
异常的要点是使气泡达到可以处理异常的代码级别。无论处于哪个级别,您都可以获得所需的信息,或者它不相关。如果有您需要的信息,但没有即时访问权限,那么您就不会在适当的级别上处理异常。
我一次想到在哪里可以提供更多信息,那就是在崩溃并发出错误转储的应用程序的绝对顶级;但是访问,编译和存储此信息并非例外。
我并不是说您可以随处放置“ throw new Exception”并将其称为好,这是可能的编写不好的异常。但是包含无关信息并不一定要正确使用它们。
评论
尽管我确实理解并同情您的观点,但我不会将丢失的数据库表的名称描述为无关紧要的。我给了你一票。
–丹尼尔·霍林拉克(Daniel Hollinrake)
15年4月20日在7:24
@DanielHollinrake是的,表名绝对与示例无关。在我使用的代码中,这种问题非常令人沮丧,看看表名如何处理就可以找出问题所在。我一直在想一个例子,说明为什么这些事情与例外无关。也许我可以用这个...
–奥达里克
2015年4月20日在11:23
我不同意。为了处理异常,可能不需要其他详细信息,因为您只是想保护应用程序免于崩溃,但最终您需要说出为什么它不起作用并能够对其进行修复。如果除了异常类型外没有其他线索,那么请调试一下。
–t3chb0t
2015年11月4日,12:14
@ t3chb0t这就是异常,内存转储以及其他所有内容的堆栈跟踪和类。如果客户打来电话并说:“计算机告诉我它试图访问索引5,但它不在那儿,它将如何为您提供帮助。”。仅当您还序列化列表,列表的上下文以及其他可能有用的内容时,它才有帮助。您不应在每次引发异常时序列化应用程序的整个状态。
–奥达里克
2015年11月4日15:35
@Odalrick对客户而言可能没有任何意义,但是作为开发人员,我重视我可以获得的每条信息;-)然后是另一个示例:假设发生了SqlExeption,这就是您所知道的一切...修复起来要容易得多如果您知道哪个连接字符串或数据库或表等不起作用....,并且如果您的应用程序使用多个数据库,则这一点尤为重要。详细的堆栈跟踪要求将pdb文件与应用程序一起提供……并非总是可能的。此外,内存转储可能会变得非常大,无法始终进行传输。
–t3chb0t
2015年11月4日17:08
评论
应该注意的是,从安全专业人员的角度来看,“错误消息不应包含有关系统内部的详细信息”是一个经验法则。@Telastyn:仅当您的系统对攻击者开放时。例如,如果您正在运行Web服务器,则希望向终端用户提供平淡的错误消息,但仍然希望在终端记录非常详细的错误消息。在用户不是攻击者的客户端软件上,您绝对希望尽可能详细地显示这些错误消息,以便在出现问题并向您发送错误报告时,您拥有尽可能多的信息可以使用。尽可能的,因为很多次您就可以得到。
@Snowman:如果是客户端软件,用户将无法访问哪些内容?机器的所有者拥有机器,并且可以购买任何东西。
相关阅读:如何编写良好的异常消息,对糟糕的错误消息的正确响应是什么?
总是有一些您想要的其他信息。我发现您作为示例给出的消息相当不错。您可以使用它们调试问题。远远优于“错误0x80001234”(受Windows Update启发的示例)。