为什么许多异常消息不包含有用的详细信息?


220

似乎在一定程度上同意异常消息应包含有用的细节

为什么系统组件中的许多常见异常不包含有用的详细信息?

一些例子:

  • .NET List索引访问ArgumentOutOfRangeException没有告诉我这是尝试和无效的索引值,也不会告诉我的允许范围。
  • 基本上,来自MSVC C ++标准库的所有异常消息都是完全无用的(与上述相同)。
  • 甲骨文例外.NET,告诉你(意译)“表或视图未找到”,但没有一个。

因此,在我看来,在大多数情况下,异常消息没有包含足够有用的细节。我的期望与众不同吗?我是否使用异常错误而我甚至注意到了这一点?或者,也许我的印象是错误的:大多数的异常实际提供有用的信息?


59
应该注意的是,从安全专业人员的角度来看,“错误消息不应包含有关系统内部的详细信息”。
Telastyn

118
@Telastyn:仅当您的系统对攻击者开放时。例如,如果您正在运行Web服务器,则希望向终端用户提供平淡的错误消息,但仍然希望在终端记录非常详细的错误消息。在用户不是攻击者的客户端软件上,您绝对希望尽可能详细地显示这些错误消息,以便在出现问题并向您发送错误报告时,您可以使用的信息更多。尽可能的,因为很多时候就可以得到。
梅森惠勒2015年

9
@Snowman:如果是客户端软件,用户将无法访问什么?机器的所有者拥有机器,并且可以得到任何东西。
梅森惠勒


6
总是有一些您想要的其他信息。我发现您提供的示例信息非常好。您可以使用它们调试问题。远胜于“错误0x80001234”(受Windows Update启发的示例)。
usr

Answers:


204

异常不包含有用的细节,因为在软件工程领域内,异常的概念还不够成熟,因此许多程序员无法完全理解它们,因此不能正确对待它们。

是的,IndexOutOfRangeException应该包含超出范围的精确索引,以及在抛出该索引时有效的范围,对于.NET运行时的创建者而言,它不是可鄙的。是的,Oracle的table or view not found异常应包含未找到的表或视图的名称,并再次代表不负责任的人可以轻视它的事实。

很大程度上,这种混淆源于误导性的原始观念,即异常应该包含人类可读的消息,而这又又是由于缺乏对异常的含义的理解,因此这是一个恶性循环。

由于人们认为该异常应包含一条人类可读的消息,因此他们认为该异常所携带的任何信息也应被格式化为该人类可读的消息,然后他们要么无聊就写了所有人类可读的消息,构建代码,否则他们担心这样做可能会将不明智的信息泄露给任何窥探者可能看到的消息。(其他答案提到的安全性问题。)

但是,事情的真相是,他们不应该担心,由于异常应包含人类可读的消息。异常是只有程序员才能看到和/或处理的东西。如果需要将故障信息呈现给用户,则必须以非常高的级别,以一种复杂的方式并以用户的语言来完成,从统计上讲,这不太可能是英语。

所以,对我们的程序员,在异常的“消息”是类名称的异常,以及其它信息相关的异常应该被复制到异常对象(最终/只读)成员变量。优选地,每个可能的一点点。这样,不需要(或不应该)生成任何消息,因此,任何撬动的眼睛都看不到它。

为了解决托马斯·欧文斯在以下评论中表达的关注:

是的,当然,在某种程度上,您创建有关该异常的日志消息。但是您已经看到了问题所在:一方面,没有堆栈跟踪的异常日志消息没有用,但另一方面,您不想让用户看到整个异常堆栈跟踪。同样,这里的问题是我们的观点受到传统做法的歪曲。传统上,日志文件采用纯文本格式,这在我们尚处于起步阶段时可能还不错,但也许不再适用:如果出于安全考虑,则日志文件必须为二进制和/或加密的。

不管是二进制文本还是纯文本,都应将日志文件视为应用程序将调试信息序列化到其中的流。这样的流只会供程序员使用,并且为异常生成调试信息的任务应该与将异常序列化到调试日志流中一样简单。这样,通过查看日志,您可以看到异常类名称(正如我已经说过的,它实际上是“消息”,)每个异常成员变量都描述了与以下内容相关的所有内容:并在实际中包含日志,以及整个堆栈跟踪。请注意,此过程中如何明显地缺少人类可读异常消息的格式。

聚苯乙烯

我可以在以下答案中找到关于该主题的更多想法:如何编写良好的异常消息

PPS

我对二进制日志文件的建议似乎使很多人不满意,因此我再次修改了答案,以使我更加清楚地表明,我在这里建议的不是日志文件为二进制,而是如果需要,日志文件可以是二进制文件。


4
我查看了.NET Framework中的某些异常类,事实证明,有很多机会可以通过编程方式添加此类信息。因此,我想这个问题将解决为“为什么不这样做”。但对于整个“人类可读”的事物,则为+1。
罗伯特·哈维

7
我不同意例外不应包含人类可读的组件。在某种程度上,您可能想要创建有关异常的日志消息。我认为将堆栈跟踪记录到用户可读的日志文件中会暴露您不想公开的实现细节,因此应该记录人类可读的消息。当提供包含错误的日志文件时,开发人员应该有一个起点来开始调试并能够强制发生异常。易于阅读的组件应适当详细描述,而无需放弃实现。
托马斯·欧文斯

44
程序员不是人类吗?看着我的同事,这证实了我一段时间以来的怀疑……
gbjbaanb 2015年

51
同样,只要软件是客户端,让用户看到整个堆栈跟踪就没有错。我曾经从事的每个专业软件项目,以及大多数业余软件项目,都包含一个日志记录系统,当引发未处理的异常时,该系统会生成完整的错误转储,包括对当前所有正在运行的线程的完整堆栈跟踪处理。用户可以(惊天动地,恐怖!)随时查看它,因为这是必要的(不仅有用,而且是必需的),以便将错误消息发送回给我们!怎么了
梅森惠勒2015年

13
对于仅二进制日志文件,我也不满意。主要是因为我在systemd方面的经验。它用于查看这些日志的特殊工具非常令人困惑,并且似乎是由莎士比亚的猴子委员会设计的。考虑到,对于Web应用程序,第一个看到您的异常的人通常是sysadmin,他将要确定是否需要修复此问题(例如磁盘空间不足)或回传给开发商。
迈克尔·汉普顿

46

为什么系统组件中的许多常见异常不包含有用的详细信息?

根据我的经验,有很多原因导致异常不包含有用的信息。我希望这些原因也适用于系统组件-但我不确定。

  • 注重安全的人员将异常视为信息泄漏的来源(例如)。由于异常的默认行为是向用户显示该信息,因此程序员有时会谨慎行事。
  • 在C ++中,我听说过反对在catch块中分配内存的争论(至少有一些上下文可以发出良好的消息)。这种分配很难管理,而且更糟的是,可能导致该处的内存不足异常-经常使您的应用程序崩溃或内存泄漏。在不分配内存的情况下很难很好地格式化异常,而且这种做法可能已经像程序员一样在语言之间迁移了。
  • 他们不知道。我的意思是,在某些情况下,代码不知道出了什么问题。如果代码不知道-无法告诉您。
  • 我曾在一些本地化问题阻止将仅英语字符串放入系统中的地方工作-即使是只有英语支持人员才能阅读的例外情况。
  • 在某些地方,我已经看到异常的使用更像断言。他们在开发期间向您发出清晰,响亮的信息,表示某件事未完成,或者已在一个地方进行了更改,但在另一个地方没有进行更改。这些通常是足够独特的,以至于好的信息将是重复的努力或仅仅是令人困惑。
  • 人们很懒,程序员比大多数人都懒。我们在例外道路上花费的时间比幸福道路少得多,这是副作用。

我的期望与众不同吗?我是否使用Exceptions错误,甚至注意到了这一点?

金田 我的意思是,例外应该有很好的信息,但它们也是例外。您应该花时间设计代码来避免出现异常情况,或者编写代码来处理异常情况(忽略消息),而不是在编码时将它们用作一种交互式反馈机制。不可避免地将它们用于调试目的,但在大多数情况下应将其保持在最低水平。您注意到此问题使我担心您在阻止它们方面做得不够好。


我知道它已被标记为C,但是我要补充一点,您的最后一段并不适用于所有语言,因为某些语言可能(正确或错误)严重依赖基于异常的错误处理报告。
Lilienthal 2015年

1
@Lilienthal-如?我所不熟悉的语言不会定期执行此类操作。
Telastyn

1
我认为这个答案有很多很好的内容,但是它避免了说“他们应该”的底线。
djechlin

3
谢谢。您的担心是没有根据的(我希望:-)。但是每次我花一分钟的时间来查找该单元测试中发生了什么问题或者花更多的时间来分析代码(因为日志文件缺少信息)时,我都会为某些消息的可避免性而烦恼:-)
Martin Ba

@Telastyn SAP的专有ABAP具有异常类构造,可以包含一条消息,该消息基本上是专门用于向用户报告程序状态(成功,失败,警告)的对象,以及一条动态(多语言)消息。我承认我不知道这种事情有多广泛,或者是否有可能在语言中得到鼓励,但至少有一种(令人遗憾的)普遍做法。
Lilienthal 2015年

12

我没有过多的C#经验,也没有专门的C ++,但是我可以告诉您-开发人员编写的异常(十分之九)比您所能找到的任何通用异常都有用。

理想情况下是的,泛型异常将为您指出错误发生的确切原因,并且您可以轻松地解决它-但实际上,在具有多个类的大型应用程序中,该类可能引发各种不同种类的异常,或者同样的异常,编写自己的输出以返回错误总是比依赖默认消息更有价值。

之所以应该这样,是因为许多人已经指出,出于安全原因或为了避免混淆,某些应用程序不想抛出不希望用户看到的错误消息。

相反,您应该在设计中预见应用程序中可能会引发哪些类型的错误(并且总会出现错误),并编写可帮助您识别问题的错误捕获消息。

这并不总能为您提供帮助-因为您无法始终预期出什么错误消息将是有用的-但从长远来看,这是更好地了解自己的应用程序的第一步。


7

问题是专门询问为什么“系统组件”(又称标准库类)引发的这么多异常不包含有用的细节。

不幸的是,大多数开发人员没有在标准库中编写核心组件,详细的设计文档或其他设计原理也不一定要公开。换句话说,我们可能永远无法确定。

但是,关于为何可能不希望或不希望详细的异常信息为何要记住两个关键点:

  1. 可以通过任何方式调用代码来使用异常:标准库无法对使用异常的方式施加约束。具体地,可以将其显示给用户。考虑数组索引超出范围:这可能为攻击者提供有用的信息。语言设计人员不知道应用程序将如何使用抛出的异常,甚至不知道应用程序的类型(例如Web应用程序或桌面应用程序),因此从安全性的角度来看,遗漏信息可能更安全。

  2. 异常不应显示给用户。而是显示友好的错误消息,并将异常记录到攻击者无法访问的位置(如果适用)。一旦发现错误,开发人员应调试代码,检查堆栈框架和逻辑路径。在这一点上,开发人员拥有比异常所希望拥有的更多的信息。


3
1.根据此标准,显示什么信息(“索引超出范围”,堆栈跟踪)和未显示(索引值)似乎相对任意。2.当已知相关的动态值时,调试可能会更快,更容易。例如,它经常会立即告诉您问题是出在失败的代码片段上的是垃圾输入,还是代码无法正确处理良好的输入
Ben Aaronson 2015年

@BenAaronson异常的标识/类告诉我们错误的类型。我的观点是,出于安全性考虑,可能会省略细节(即导致错误的特定值)。该值可以追溯到用户输入,从而向攻击者透露信息。

@Snowman我几乎不认为当有完整的堆栈跟踪可用而索引号没有可用时,安全性不是考虑因素。我肯定知道攻击者正在探查缓冲区溢出,但是许多异常也忽略了相当安全的数据(例如,未找到哪个Oracle表)
gbjbaanb 2015年

@gbjbaanb谁说我们需要向用户显示完整的堆栈跟踪信息?

1
感谢您分享这一见解。就我个人而言,我不同意并且认为安全性论点是完全谬论,但这可能是有道理的。
马丁·巴

4

首先,让我破灭一个泡沫,说即使在诊断消息中加载了在4秒内将您带到准确的代码行和子命令的信息,用户也有可能永远不会将其写下或传达给支持人员然后您会被告知“它说了一些违规行为……我不知道它看起来很复杂!”

我从事软件开发和支持其他软件的研究已有30多年了,就个人而言,异常消息的当前质量实际上与安全性无关,无论最终结果如何符合他们的模型。与宇宙有关的事实更多,这是因为我们行业中的许多人本来都是自学成才的,他们从来没有在交流中上过课。也许如果我们将所有新的编码器强制为维护职位几年,而他们不得不处理找出错误的地方,他们将了解至少某种形式的精度的重要性。

最近,在正在重建的应用程序中,决定将返回码归为以下三类之一:

  • 0到9是成功的附加成功信息。
  • 10到99是非致命(可恢复)错误,并且
  • 101到255是致命错误。

(由于某种原因遗漏了100个)

在任何特定的工作流程中,我们的思想都被重用了,否则通用代码将使用通用返回值(> 199)来处理致命错误,从而使我们在工作流程中可能出现100个致命错误。如果消息中的数据略有不同,则诸如找不到文件之类的错误都可能使用相同的代码并与消息区分开。

当代码从承包商那里回来时,当几乎所有的致命错误都是返回代码101时,您将不会感到惊讶。

考虑到所有这些,我认为您的问题的答案是消息是如此无意义,因为最初创建消息时,它们是占位符,没有人回过头来。最终,人们想出了解决问题的方法,而不是因为出现消息而已,而是发现消息。

从那时起,自学成才的人就从来没有一个关于异常应该包含什么的好例子。除此之外,更多的用户不阅读消息,更少地尝试将其传递给支持(我看到过的错误消息是由使用过的用户剪切并粘贴的,然后经过删除后再发送,并带有稍后的注释。似乎有很多信息,而我可能并不需要全部,因此他们随机删除了一堆。

让我们面对现实吧,如果有更多的工作量并且不增加闪存,那么下一代编码器的数量就会太多(不是全部,而是太多),这是不值得的……

最后说明:如果错误消息中包含错误/返回代码,在我看来,在已执行模块中的某处应该有一行,内容类似于“ if condition return code-value”,并且condition应该告诉您为什么返回代码发生了。这似乎是一种简单的逻辑方法,但是对我而言,请尝试让Microsoft告诉您,当Windows升级失败时,CODE 80241013或其他一些非常独特的标识符发生了什么。有点伤心,不是吗?


8
“ ...当几乎每个致命错误都返回代码101时,您不会相信我们会感到惊讶。” 你是对的。我不会相信承包商对您的指示感到惊讶。
Odalrick

3
chances are the users will never write it down... and you will be told "Well it said something about a violation..." 这就是为什么您使用异常日志记录工具自动生成包含堆栈跟踪的错误报告,甚至可能将其发送到服务器的原因。我曾经有一个用户不是很熟练。每当她每次从错误记录器提交一条消息时,都会出现类似“我不确定我做错了什么,但是...”之类的事情,无论我解释了多少次错误表明该错误就在我身边。 。但是,我总是收到她的错误报告!
梅森惠勒2015年

3

虽然我确实同意,例外应该包含尽可能多的信息,或者至少应包含较少的通用信息。在找不到表的情况下,表名会很好。

但是,您在收到异常的代码中更了解要执行的操作。当您无法控制的库中出现问题时,您通常通常通常并不能采取很多措施来纠正这种情况,但是您可以添加更多有用的信息,以了解在什么情况下出现了问题。

在找不到表的情况下,如果被告知无法找到的表称为STUDENTS,这将无济于事,因为您没有这样的表,并且该字符串在代码中不存在。

但是,如果捕获到异常并使用尝试执行的SQL语句将其重新抛出,那么情况会更好,因为事实证明,您试图插入名称字段为Robert')的记录;拖放表学生;(总会有一个xkcd!)

因此,要与少见的信息性例外作斗争:试试try-catch-rethrow并提供与您要执行的操作有关的更多信息。

我可能应该补充一点,因为这是关于问题原因的更多答案,原因是图书馆制造者关注的焦点不是一直在使异常消息更好,而他们却不知道为什么尝试失败的原因是调用代码中包含了该逻辑。


例如在C ++中,您应该使用throw_with_nested很多。
Miles Rout 2015年

3

给出一个稍微不同的答案:可能已经对规范进行了攻击:

  • 该函数接收X并返回Y
  • 如果X无效,则引发异常Z

加上在最短的时间内以最小的麻烦来准确交付规格的压力(以免在审查/测试中被拒绝),那么您就有了制定完全合规且无济于事的库例外的诀窍。


3

异常具有特定于语言和实现的成本。

例如,需要C ++异常来销毁在抛出调用帧和捕获调用帧之间的所有活动数据,这很昂贵。因此,程序员不希望大量使用异常。

在Ocaml中,异常抛出的速度几乎与C一样快setjmp(其代价不取决于遍历的调用帧的数量),因此开发人员可以大量使用它(即使在非异常,非常常见的情况下)。相反,C ++异常足够繁重,因此您可能不会像在Ocaml中那样大量使用它们。

一个典型的示例是一些递归搜索或探索,可以在相当深的递归中“停止”(例如,在树中找到叶子,或使用统一函数)。在某些语言中,将这种条件传播给每个呼叫者的速度更快(因此更为惯用)。在其他语言中,抛出异常更快。

因此,根据语言(以及开发人员使用它的习惯)的不同,异常可能包含很多有用的细节,或者相反,它可以用作快速的非本地跳转,并且仅包含非常有用的数据。


6
“例如,需要C ++异常来销毁在抛出调用框架和捕获调用框架之间的所有活动数据,这很昂贵。因此,程序员不希望过多使用异常。” 总废话。C ++异常的主要优点是确定性调用了析构函数。如果他们跳了,没人会用它们。
Miles Rout 2015年

我知道,但是事实是C ++异常不像Ocaml,并且您不会像在Ocaml中那样使用它们。
Basile Starynkevitch 2015年

5
请勿将C ++异常用于C ++中的控制流,这主要是因为这样做会使代码难以阅读且难以理解。
Miles Rout 2015年

-2

是什么让您认为索引值或所需范围或表名称有用的详细信息(例外)?

异常不是错误处理机制。它们是一种恢复机制。

异常的要点是使气泡达到可以处理异常的代码级别。无论处于哪个级别,您可以获得所需的信息,或者它不相关。如果有您需要的信息,但没有立即访问权限,则您不会在适当的级别上处理异常。

我可以想到的是,有什么地方可以提供更多的信息,是在应用程序的绝对顶层,您崩溃并发出错误转储。但是访问,编译和存储此信息并非例外。

我并不是说您可以随处放置“引发新异常”并称其为好,有可能编写不良的异常。但是,包含无关信息并不一定要正确使用它们。


尽管我确实理解并同情您的观点,但我不会将丢失的数据库表的名称描述为无关紧要的。我给了你一票。
Daniel Hollinrake

1
@DanielHollinrake是的,表名绝对与示例无关。在我使用的代码中,这种问题非常令人沮丧,看看表名如何处理就可以找出问题所在。我一直在想一个例子,说明为什么这些事情与例外无关。也许我可以用这个...
Odalrick

我不同意。为了处理异常,可能不需要其他详细信息,因为您只是想保护应用程序免于崩溃,但最终您需要说出为什么它不起作用并能够对其进行修复。如果除了异常类型外没有其他线索,那么请调试一下。
t3chb0t 2015年

@ t3chb0t这就是异常,内存转储以及其他所有内容的堆栈跟踪和类。如果客户打来电话并说:“计算机告诉我它试图访问索引5,但它不在那儿,它将如何为提供帮助。” 它可能只帮助,如果你也序列化列表,该列表和其他任何可能有用的上下文。您不应在每次引发异常时序列化应用程序的整个状态。
Odalrick

@Odalrick对于客户而言可能没有任何意义,但是作为开发人员,我重视我可以获得的每条信息;-)然后是另一个示例:假设发生了SqlExeption,这就是您所知道的一切... 修复起来要容易得多如果您知道哪个连接字符串或数据库或表等不起作用....,并且如果您的应用程序使用多个数据库,则这一点尤为重要。详细的堆栈跟踪要求将pdb文件与应用程序一起提供……并非总是可能的。而且,内存转储会变得非常大,无法始终进行传输。
t3chb0t
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.