异常传播:何时应捕获异常?


44

MethodA调用MethodB,然后依次调用MethodC。

在MethodB或MethodC中没有异常处理。但是在MethodA中有异常处理。

在MethodC中发生异常。

现在,该异常正在冒泡到MethodA,该方法可以适当地对其进行处理。

这有什么问题?

在我看来,在某个时候调用者将执行MethodB或MethodC,并且当这些方法中确实发生异常时,从这些方法内部处理异常中可以获得什么,这实际上只是一个try / catch / finally块,而不仅仅是让他们冒泡给被呼叫者?

围绕异常处理的陈述或共识是,仅在执行不能继续执行时抛出该异常。我明白了。但是,为什么不在链的更深处捕获异常,而不是让try / catch块一直向下传播。

当您需要释放资源时,我会理解的。完全是另一回事。


46
您为什么认为共识是要有一系列的通过渔获量?
卡雷斯

使用良好的IDE和适当的编码样式,您可以知道在调用方法时会引发某些异常。处理或允许其传播是调用者的决定。我没有发现任何问题。
Hieu Le

14
如果一个方法不能处理该异常,而只是将其重新抛出,我会说这是一种代码味道。如果某个方法不能处理该异常,并且在引发异常时不需要执行任何其他操作,则根本不需要try-catch块。
Greg Burghardt

7
“这怎么了?” :没事
伊万

5
直通捕获(不会将异常包装为不同类型或类似的东西)破坏了异常的全部目的。异常抛出是一种复杂的机制,它是有意构建的。如果传递捕获是预期的用例,那么您所要做的就是实现一个Result<T>类型(一种存储计算结果或错误的类型),然后从其他抛出函数返回它。在堆栈中传播错误将需要读取每个返回值,检查其是否为错误,如果是则返回错误。
亚历山大

Answers:


139

作为一般原则,除非您知道如何处理异常,否则不要捕获异常。如果MethodC抛出异常,但是MethodB没有有用的方法来处理它,则它应允许异常传播到MethodA。

方法应具有捕获和重新抛出机制的唯一原因是:

  • 您想要将一种异常转换为对上面的调用者更有意义的另一种异常。
  • 您想要向异常添加其他信息。
  • 您需要一个catch子句来清理没有一个资源就会泄漏的资源。

否则,在错误的级别捕获异常会导致代码默默地失败,而不会向调用代码(以及最终的软件用户)提供任何有用的反馈。捕获异常然后立即将其重新抛出的替代方法是没有意义的。


28
@GregBurghardt如果你的语言有一个类似于try ... finally ...,然后使用,不赶及重新抛出
Caleth

19
“捕获异常然后立即将其重新抛出是没有意义的”,具体取决于语言和处理方式,可能会对代码库产生有害影响。经常尝试这样做的人会删除很多有关异常的信息,例如原始的堆栈跟踪。我已经处理了代码,其中调用者得到一个异常,该异常对于发生的情况和发生的位置完全具有误导性。
JimmyJames

7
“除非您知道如何处理异常,否则不要捕获异常”。乍一看听起来很合理,但后来却引起问题。您在这里所做的是将实现细节泄漏给呼叫者。假设您在实现中使用特定的ORM来加载数据。如果您没有捕获到ORM的特定异常,而只是让它们冒出来,您将无法替换数据层而不破坏与现有用户的兼容性。这是最明显的情况之一,但它可能会变得非常隐蔽并且难以发现。
Voo

11
@Voo在您的例子,你知道该怎么做。将其包装在特定于您的代码的已记录异常中,例如,LoadDataException并根据您的语言功能包括原始异常详细信息,以便将来的维护人员能够查看根本原因,而不必附加调试器并弄清楚如何重现该问题。
科林·杨

14
@Voo您似乎错过了“捕获/重新抛出”场景的“您想将一个异常转换为对上述调用者更有意义的另一个异常”的原因。
jpmc26

21

这有什么问题?

绝对没有。

现在,该异常正在冒泡到MethodA,该方法可以适当地对其进行处理。

“妥善处理”是重要的部分。这就是结构化异常处理的症结所在。

如果您的代码可以通过Exception做“有用的”事情,那就去做吧。如果没有,那就别说了。

。。。为什么不在链的更深处捕获异常,而不是一直将try / catch块一直拖下去。

那正是你应该做的。如果您正在阅读的处理程序/重新抛出器“一直向下”的代码,那么您(可能)正在阅读一些非常糟糕的代码。

可悲的是,有些开发人员只是将catch块视为他们编写的每个方法都放入的“样板”代码(没有双关语),通常是因为他们并没有真正“获取”异常处理,并认为他们必须添加一些东西。例外不会“逃脱”并杀死他们的程序。

这里的困难的部分原因是,大部分的时间,这个问题甚至不会注意到,因为异常没有被抛出的所有的时间,但,当他们,该程序会浪费非常多的时间和逐步取消调用堆栈,直至到达对Exception确实有用的地方。


7
更糟糕的是,当应用程序捕获到异常,然后将其记录下来(希望它不会永远存在于此)并尝试照常继续进行,即使它确实不能继续执行。
所罗门·乌科

1
@SolomonUcko:好吧,这取决于。例如,如果您正在编写一个简单的RPC服务器,并且未处理的异常一直冒泡到主事件循环,那么您唯一合理的选择是对其进行记录,将RPC错误发送给远程对等方,然后继续处理事件。另一种选择是杀死整个程序,这将在服务器死于生产时驱动您的SRE。
凯文

@Kevin在这种情况下,应该尽可能catch在最高级别记录一个日志并返回错误响应。没有catch块到处撒。如果您不想列出所有可能的已检查异常(使用Java之类的语言),只需将其包装在中,RuntimeException而不是将其记录在其中,尝试继续操作,并遇到更多错误甚至漏洞。
所罗门·乌科

8

您必须在库和应用程序之间有所作为。

图书馆可以自由引发未捕获的异常

设计库时,有时必须考虑可能出什么问题。参数的范围可能错误或null,外部资源可能不可用,等等。

您的图书馆通常没有办法以明智的方式处理它们。唯一明智的解决方案是引发适当的Exception,并让Application的开发人员处理它。

应用程序应始终在某些时候捕获异常

当捕获到异常时,我喜欢将它们归类为Errors致命错误。常规错误表示我的应用程序中的单个操作失败。例如,由于目标不可写,因此无法保存打开的文档。应用程序要做的唯一明智的想法是通知用户操作无法成功完成,提供有关该问题的人类可读信息,然后让用户决定下一步要做什么。

致命错误是一个错误的主应用程序逻辑不能从恢复。例如,如果图形设备驱动程序在视频游戏中崩溃,则该应用程序无法“优雅地”通知用户。在这种情况下,应写入日志文件,并且如果可能,应以某种方式通知用户。

即使在这种严重情况下,应用程序也应该以有意义的方式处理此异常。这可能包括编写日志文件,发送崩溃报告等。应用程序没有理由不以某种方式响应异常。


确实,如果您有一个用于磁盘写操作或其他硬件操作之类的库,则可能会遇到各种意外事件。如果在写入过程中拔出硬盘驱动器怎么办?什么CD驱动器在读取时短路?那是您无法控制的,尽管您可以做一些事情(例如,假装成功),但通常最好的做法是向库用户抛出异常并让他们做出决定。也许HDDPluggedOutDuringWritingException可以处理,对于应用程序来说并不致命。该程序可以决定如何处理。
VLAZ

1
@VLAZ是致命还是非致命是应用程序必须决定的事情。图书馆应该告诉发生了什么。应用程序必须决定如何对其做出反应。
MechMK1

0

您描述的模式有什么问题,即方法A无法区分三种情况:

  1. 方法B以预期的方式失败。

  2. 方法C失败的方式不是方法B所预期的,而是方法B正在执行可以安全地放弃的操作时。

  3. 方法C失败的方式不是方法B所预期的,而是方法B执行操作时,将事物置于假定为暂时的非相干状态,由于B的失败,B无法清理该状态。

方法A能够区分这些情况的唯一方法是,如果从B抛出的异常包括足够用于该目的的信息,或者方法B的堆栈展开导致对象处于显式无效状态。不幸的是,大多数异常框架使这两种模式都变得笨拙,迫使程序员做出“更少邪恶的”设计决策。


2
方案2和3是方法B中的错误。方法A不应尝试解决这些问题。
Sjoerd

@Sjoerd:方法B应该如何预期方法C可能失败的所有方式?
超级猫

通过众所周知的模式,例如执行可能会扔到临时变量中的所有操作,然后执行无法抛出的操作(例如,交换),将旧状态替换为新状态。另一种模式是定义可以安全重复的操作,因此您可以重试该操作而不必担心弄乱。有关写“异常安全代码”的完整书籍,所以我在这里不能告诉您所有信息。
Sjoerd

这是完全不使用异常的好方法(这是一个很棒的决定,恕我直言)。但我想,它并没有真正回答这个问题,因为OP似乎在首位使用异常的意图,只要求其中渔获应该的。
cmaster

@Sjoerd方法B变得更容易推断语言是否禁止例外。因为在那种情况下,您实际上看到了所有通过B的控制流路径,因此您不必再猜测哪些操作符可能会以抛出方式(C ++)重载来避免场景3。我们在代码方面付出了很多代价清晰和安全,以免因“只是”抛出异常而返回错误。因为最后,错误处理代码的重要组成部分。
cmaster
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.