谁应该阅读Exception.Message(如果有的话)?


27

在设计异常时,我应该编写用户或开发人员应该理解的消息吗?谁真正应该是异常消息的阅读者?

我发现异常消息根本没有用,我总是很难编写它们。按照约定,异常的类型应该已经告诉我们为什么某些事情不起作用,并且自定义属性可能会添加更多信息,例如文件名,索引,键等。那么为什么在消息本身中重复它呢?自动生成的消息也可以执行此操作,它所必须包含的就是带有其他属性列表的异常名称。这和手写文本一样有用。

根本不编写消息,而是使用特殊的异常渲染器来处理可能创建于不同语言中的有意义的消息,而不是用代码对其进行硬编码,这会更好吗?


我被问到这些问题中的任何一个是否都可以回答我的问题:

我读过他们两个,但对他们的回答感到不满意。他们通常谈论用户,并着重于消息本身的内容,而不是收件人,事实证明,至少可以有两个用户:最终用户和开发人员。我永远不知道在编写异常消息时应该和哪个人说话。

我什至认为这条著名的消息根本没有任何真正的价值,因为它只是用不同的词来重复异常类型的名称,所以为什么还要费心编写它们呢?我可以完美地自动生成它们。

对我而言,异常消息缺乏读者的区分。一个完美的例外将需要提供至少两个消息版本:一个针对最终用户,一个针对开发人员。仅将其称为消息太笼统了。然后,应该用英语编写开发人员消息,但最终用户的消息可能需要翻译成其他语言。仅用一条消息是不可能实现所有这些的,因此异常将需要为最终用户消息提供一些标识符,正如我刚才所说的,该标识符可能以不同的语言提供。

当我阅读所有其他链接的问题时,我得到的印象是,异常消息的确是由最终用户而非开发人员阅读的……一条消息就像也要吃蛋糕一样。


4
在这里,我想到了关于程序员的另外两个问题。我不确定它们是否重复,但是我认为您应该阅读它们,也许它们会解决您的部分或全部担忧:如何编写良好的异常消息以及为什么许多异常消息不包含有用的详细信息?
Thomas Owens

2
@ThomasOwens我都读过它们,但是它们没有针对异常消息的特定接收者。他们通常谈论用户,但他是谁?现在是对编程有所了解的软件用户还是应该分析出问题的开发人员?我永远不确定在写消息时应该向谁讲话。
t3chb0t 2015年

2
我认为,一个很好的答案可能涉及这两个问题中的一个或两个,但它们并非重复。
与Monica进行的Lightness竞赛

1
为什么在地球上将其作为副本关闭?即使它是另一个问题的重复(不是),这个问题显然也有更好的答案,应该是另一个被标记为重复的问题。
Ben Aaronson 2015年

2
@MichaelT我还没有真正看到很多重叠存在,除非你先假定异常消息是用户
本·阿伦森

Answers:


46

这些消息是给其他开发人员的

这些消息应由开发人员阅读,以帮助他们调试应用程序。这可以采取两种形式:

  • 主动调试。您实际上是在编写代码并试图弄清楚发生什么时运行调试器。在这种情况下,有用的异常将通过使您易于理解问题所在或最终提出解决方法来指导您(尽管这是可选的)。

  • 被动调试。该代码在生产中运行并且失败。记录了异常,但是您仅收到消息和堆栈跟踪。在这种情况下,有用的异常消息将帮助您快速定位错误。

    由于这些消息经常被记录下来,因此这也意味着您不应在其中包括敏感信息(例如私钥或密码,即使对调试应用程序很有用)。

例如,IOSecurityException写入文件时引发的异常类型不是很明确地说明该问题:是否是因为我们没有访问文件的权限?还是我们可以阅读但不能写?还是文件不存在,而我们没有在那里创建文件的权限?或者它可能已被锁定(希望这种情况下异常的类型会有所不同,但实际上,有时类型可能是隐秘的)。还是代码访问安全性阻止我们执行任何I / O操作?

代替:

IOSecurityException:已找到文件,但读取其内容的权限被拒绝。

更明确。在这里,我们立即知道目录权限设置正确(否则,我们将无法知道该文件存在),但是文件级别的权限存在问题。

这也意味着,如果您无法提供异常类型以外的任何其他信息,则可以将消息保留为空。 DivisionByZeroException这是一个消息多余的好例子。另一方面,大多数语言都允许您在不指定异常消息的情况下引发异常的事实是出于不同的原因:要么是因为默认消息已经可用,要么是因为如果需要,它将在以后生成(并且生成此消息)。消息被包含在异常类型中,这很有意义,“ OOPly”

请注意,出于技术(通常是性能)方面的原因,某些消息最终变得比应有的含义更加神秘。.NET的NullReferenceException

你调用的对象是空的。

是一条无用的信息的绝佳示例。一个有用的消息是:

product在中调用时,对象引用未设置为对象的实例product.Price

这些消息不是给最终用户的!

最终用户不应看到异常消息。决不。尽管某些开发人员最终将这些消息显示给用户,但这导致糟糕的用户体验和沮丧感。消息如:

你调用的对象是空的。

对最终用户而言绝对没有任何意义,应不惜一切代价避免。

最坏的情况是使用全局try / catch,将异常抛出给用户并退出应用程序。一个关心用户的应用程序:

  • 首先处理异常。大多数情况下都可以处理而不会打扰用户。网络已关闭?为什么不等几秒钟再试一次?

  • 防止用户将应用程序引向特殊情况。如果您要求用户输入两个数字并将第一个数字除以第二个数字,那么为什么要让用户在第二种情况下输入零,以便在几秒钟后归咎于他?将文本框突出显示为红色(一个有用的工具提示,告诉数字应该不为零),然后禁用验证按钮,直到该字段保持红色该怎么办?

  • 邀请用户以没有错误的形式执行操作。没有足够的权限访问文件?为什么不要求用户授予管理权限或选择其他文件?

  • 如果没有其他效果,则显示一个有用的,友好的错误,该错误专门用于减少用户的挫败感,帮助用户了解出了什么问题并最终解决了该问题,并且还帮助他防止了以后的错误(适用时)。

    在您的问题中,您建议有两个例外消息:一种是针对开发人员的技术性消息,另一种是针对最终用户的消息。尽管这在某些较小的情况下是一个有效的建议,但是大多数例外是在不可能为用户产生有意义的消息的级别上产生的。以DivisionByZeroException想象,我们无法阻止发生的异常,并不能处理它自己。发生划分时,框架(因为是框架,而不是引发异常的业务代码)知道对用户有帮助的消息是什么?绝对不:

    被零除。[好]

    相反,可以让它引发异常,然后在我们了解业务上下文的更高级别捕获它,并可以采取相应措施以实际帮助最终用户,从而显示出类似以下内容:

    字段D13的值不能与字段E6中的值相同,因为这些值的减法用作除数。[好]

    或许:

    ATP服务报告的值与本地数据不一致。这可能是由于本地数据不同步引起的。您要同步运输信息并重试吗?[是] [否]

这些消息不用于解析

异常消息也不希望被解析或以编程方式使用。如果您认为调用方可能需要其他信息,请将其与消息一起包含在异常中。这很重要,因为消息如有更改,恕不另行通知。类型是接口的一部分,但消息不是:永远不要依赖它进行异常处理。

想象一下异常消息:

等待500毫秒后,连接到缓存服务器超时。要么增加超时时间,要么检查性能监视以发现服务器性能下降。缓存服务器的平均等待时间为6毫秒。最后一个月4毫秒 最后一周和377毫秒。最后一个小时。

您要提取“ 500”,“ 6”,“ 4”和“ 377”值。考虑一下您将用于进行解析的方法,然后继续阅读。

你有主意吗?大。

现在,原始开发人员发现了一个错字:

Connecting to the caching sever timed out after waiting [...]

应该:

                            ↓
Connecting to the caching server timed out after waiting [...]

此外,开发人员认为月/周/一个小时不是特别重要,因此他还进行了其他更改:

缓存服务器的平均等待时间为6毫秒。最后一个月5毫秒。在过去的24小时和377毫秒中。最后一个小时。

您的解析会怎样?

除了解析外,您还可以使用异常属性(可以在序列化数据后立即包含异常):

{
    message: "Connecting to the caching [...]",
    properties: {
        "timeout": 500,
        "statistics": [
            { "timespan": 1, "unit": "month", "average-timeout": 6 },
            { "timespan": 7, "unit": "day", "average-timeout": 4 },
            { "timespan": 1, "unit": "hour", "average-timeout": 377 },
        ]
    }
}

现在使用这些数据有多容易?

有时(例如在.NET中),消息甚至可以翻译成用户的语言(恕我直言,翻译这些消息是绝对错误的,因为任何开发人员都希望能够用英语阅读)。解析此类消息几乎是不可能的。


2
关于最终用户的好处。我还要补充一点,出于安全原因,最终用户也不应看到异常消息。向非外行用户了解错误的确切性质后,您可以利用它。最终用户应该只在他或她正在做的事情的上下文中知道错误。
尼尔

1
@Neil:有点太理论化了。实际上,在线日志的复杂性超过了对用户隐藏日志的好处。不算用户友好方面。假设您要在新的Windows VM上安装Visual Studio。突然,安装失败。您是希望自己去查看日志并自行诊断问题(在Stack Exchange和博客的帮助下)还是等到Microsoft解决问题并发布修补程序后再继续?
Arseni Mourzenko 2015年

4
我认为每个人都知道“对象引用...”消息没有帮助。不过,相关的问题是您希望生成哪种抖动生成所需消息的机器代码? 如果您进行此练习,您很快就会发现,如果不对所有非例外情况施加巨大的性能成本,就很难轻易生成该消息。使粗心的开发人员的工作变得稍微容易一些的好处,并不会因最终用户的性能损失而得到补偿。
埃里克·利珀特

7
关于安全例外:例外中的信息越少越好。我最喜欢的轶事是,在.NET 1.0的beta版中,您会收到一条异常消息,例如“您无权确定文件C:\ foo.txt的名称”。太好了,谢谢您提供的详细异常消息!
埃里克·利珀特

2
我不同意从未向最终用户显示详细的错误消息。我经历了很多次,我收到一条隐秘的错误消息,阻止我启动游戏/软件,但是当我用google搜索时,发现有一个简单的解决方法。没有该错误消息,将找不到解决方法。也许只将详细信息隐藏在“更多详细信息”按钮下会更好吗?
BlueRaja-Danny Pflughoeft 2015年

2

答案完全取决于异常。

要问的最重要的问题是“谁可以解决问题?” 如果该异常是由于您在打开文件时发生文件系统错误而引起的,则可以将该消息提供给最终用户。该消息可能包含有关如何解决该错误的更多信息。我可以想到一个确定的案例,在我的工作中,我有一个加载了DLL的插件系统。如果用户在命令行上键入错误以加载错误的插件,则底层系统错误消息实际上包含有用的信息,以使他们能够纠正问题。

但是,大多数情况下,用户无法修复未捕获的异常。其中大多数涉及对代码进行更改。在这些情况下,消息的使用者显然是开发人员。

被捕获的异常的情况更为复杂,因为您有机会正确处理异常并抛出更友好的异常。我写的一种脚本语言引发了异常,这些异常的信息显然是针对开发人员的。但是,随着堆栈展开,我从脚本语言中添加了用户可读的堆栈跟踪。我的用户很少能看到该消息,但是他们可以查看脚本中的堆栈跟踪并找出95%的时间发生了什么。对于剩下的5%,当他们打电话给我时,我们不仅有了堆栈跟踪,而且还有一条开发人员就绪的消息可以帮助我找出问题所在。

总之,我将异常消息视为未知内容。更上游的人,对实现有更多了解,使我的软件成为例外。有时我有一种预感,即未知的内容将对最终用户有用,有时却没有。


1
“如果异常是由打开文件时的文件系统错误引起的,则向最终用户发送该消息可能是有道理的”:但是当您抛出文件系统错误时,您不知道上下文:这可能是用户的操作,或者是完全不相关的操作(例如,您的应用对某些配置进行了备份,而用户甚至没有注意到它)。这意味着您将有一个try / catch块,其中显示了一条错误消息,这与直接显示异常消息非常不同。
Arseni Mourzenko 2015年

@MainMa您可能不明确知道上下文,但是有很多提示。例如,如果他们正在打开文件,并且异常是“ IOError:权限被拒绝”,则用户很有可能会将2和2放在一起以找出有问题的文件。我最喜欢的编辑器可以执行此操作-它将仅在对话框中输出异常文本,而不会产生任何绒毛。另一方面,如果我正在加载我的应用程序并在加载一堆图像时出现“权限被拒绝”的情况,那么假设用户可以对信息做些事情就不那么合理了。
Cort Ammon-恢复莫妮卡2015年

为您的观点而论,除了向用户显示NullReferenceException之外,我从未见过任何价值,仅显示该消息即可表明您的软件不知道如何恢复!该异常消息对最终用户永远不会有用。
Cort Ammon-恢复莫妮卡
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.