如何使用try catch进行异常处理是最佳实践


201

在维护甚至声称自己是高级开发人员的同事的代码的同时,我经常看到以下代码:

try
{
  //do something
}
catch
{
  //Do nothing
}

或者有时他们将日志记录信息写入日志文件,例如以下代码try catch

try
{
  //do some work
}
catch(Exception exception)
{
   WriteException2LogFile(exception);
}

我只是想知道他们所做的是最佳做法吗?这让我感到困惑,因为在我的思考中,用户应该知道系统会发生什么。

请给我一些建议。


128
片段1是99.999%的时间是不可接受的。
leppie

22
直接向用户显示异常从来都不是一个好主意,主要有两个原因:1.如果是普通用户,他会很讨厌阅读错误消息,但对他/她来说却很少。2.如果他是所谓的黑客,他可能会获得有用的信息。IMO的最佳实践是记录异常并显示友好的错误消息。
Leri

4
@leppie如果发生意外情况(例如NullReferenceArgumentNull不属于应用程序流程的一部分),则意味着需要修复一个错误,因此记录这些错误将有助于更快地调试您的代码。
Leri

14
使用try-catch块隐藏异常通常是惰性编程的结果。这是一种快捷方式,通常用来代替编写验证代码来测试输入。有时,可能会出现不影响代码操作的异常,因此可以将其隐藏起来。但是,这种情况很少见。
科里

12
@Toan,好吧,如果它是批处理作业,那么我正在顶层(Main)进行日志记录,然后重新抛出以发出警报,指出该作业异常终止。如果是Web应用程序,则让异常气泡进入全局处理程序,记录日志,然后将用户重定向到错误屏幕。您的用例场景规定了在记录或以其他方式处理该异常后该如何处理。
安东尼·佩格拉姆

Answers:


300

我的异常处理策略是:

  • 要通过钩住捕获所有未处理的异常Application.ThreadException event,然后决定:

    • 对于UI应用程序:使用道歉消息将其弹出给用户(winforms)
    • 对于服务或控制台应用程序:将其记录到文件(服务或控制台)中

然后,我总是将在外部运行的所有代码都包含在try/catch

  • Winforms基础结构触发的所有事件(加载,单击,SelectedChanged ...)
  • 第三方组件触发的所有事件

然后我附上“ try / catch”

  • 知道的所有操作可能并非一直有效(IO操作,可能的零除法计算...)。在这种情况下,我会抛出一个新事件ApplicationException("custom message", innerException)来跟踪实际发生的情况

此外,我会尽力正确地对异常进行排序。有以下例外情况:

  • 需要立即向用户显示
  • 需要一些额外的处理,以便在事情发生时将它们放在一起,以避免级联问题(即:finallyTreeView填充期间将.EndUpdate放在该部分中)
  • 用户不在乎,但重要的是要知道发生了什么。所以我总是记录他们:

    • 在事件日志中
    • 或磁盘上的.log文件中

设计一些静态方法来处理应用程序顶级错误处理程序中的异常是一个好习惯。

我也强迫自己尝试:

  • 请记住,所有异常都冒泡到顶层。不必将异常处理程序放在各处。
  • 可重用或深度调用的函数不需要显示或记录异常:它们要么自动冒泡,要么在我的异常处理程序中随一些自定义消息重新抛出。

所以最后:

坏:

// DON'T DO THIS, ITS BAD
try
{
    ...
}
catch 
{
   // only air...
}

无用:

// DONT'T DO THIS, ITS USELESS
try
{
    ...
}
catch(Exception ex)
{
    throw ex;
}

最终尝试而没有抓住是完全有效的:

try
{
    listView1.BeginUpdate();

    // If an exception occurs in the following code, then the finally will be executed
    // and the exception will be thrown
    ...
}
finally
{
    // I WANT THIS CODE TO RUN EVENTUALLY REGARDLESS AN EXCEPTION OCCURED OR NOT
    listView1.EndUpdate();
}

我在最高层的工作:

// i.e When the user clicks on a button
try
{
    ...
}
catch(Exception ex)
{
    ex.Log(); // Log exception

    -- OR --

    ex.Log().Display(); // Log exception, then show it to the user with apologies...
}

我在一些所谓的函数中所做的事情:

// Calculation module
try
{
    ...
}
catch(Exception ex)
{
    // Add useful information to the exception
    throw new ApplicationException("Something wrong happened in the calculation module :", ex);
}

// IO module
try
{
    ...
}
catch(Exception ex)
{
    throw new ApplicationException(string.Format("I cannot write the file {0} to {1}", fileName, directoryName), ex);
}

异常处理(自定义异常)有很多工作要做,但是我尝试记住的那些规则对于我所做的简单应用程序已经足够了。

这是扩展方法的示例,用于轻松处理捕获的异常。它们以一种可以链接在一起的方式实现,并且很容易添加您自己捕获的异常处理。

// Usage:

try
{
    // boom
}
catch(Exception ex)
{
    // Only log exception
    ex.Log();

    -- OR --

    // Only display exception
    ex.Display();

    -- OR --

    // Log, then display exception
    ex.Log().Display();

    -- OR --

    // Add some user-friendly message to an exception
    new ApplicationException("Unable to calculate !", ex).Log().Display();
}

// Extension methods

internal static Exception Log(this Exception ex)
{
    File.AppendAllText("CaughtExceptions" + DateTime.Now.ToString("yyyy-MM-dd") + ".log", DateTime.Now.ToString("HH:mm:ss") + ": " + ex.Message + "\n" + ex.ToString() + "\n");
    return ex;
}

internal static Exception Display(this Exception ex, string msg = null, MessageBoxImage img = MessageBoxImage.Error)
{
    MessageBox.Show(msg ?? ex.Message, "", MessageBoxButton.OK, img);
    return ex;
}

98
catch(Exception ex) { throw ex; }C#中的代码比冗余代码更糟糕(无论您捕获的是哪种异常类型)。要重新投掷,请使用throw;。对于前者,异常看起来像是源于您的,throw ex而对于后者,则异常将恰好源于原始throw语句。
CVn

2
您为什么要钩挂Application.ThreadException事件并用包裹每个异常catch(Exception ex) {ex.Log(ex);}。我可能会同意,前者是一种很好的做法,但后者会增加复制错误日志的风险,并掩盖了发生异常的情况。也是throw ex非常非常糟糕。
基思

1
我了解catch(Exception ex){throw ex; }毫无用处。因此,我认为“冗余”不是陈述“不要这样做”的最佳词。这就是为什么我稍稍更改了帖子以更好地说明必须避免使用try catch的两个第一个示例的原因。
拉里

3
伟大而富有建设性的答案,最重要的是,我最喜欢短语“ 仅空气 :)”。感谢这次Application.ThreadException活动,我没有意识到这一点,非常有用。
Mahdi Tahsildari 2014年


61

最佳实践是,异常处理永远都不应隐藏问题。这意味着try-catch块应该非常罕见。

在3种情况下使用try-catch有意义。

  1. 始终尽可能地处理已知异常。但是,如果您期望出现异常,通常最好先进行测试。例如,解析,格式化和算术异常总是总是最好先通过逻辑检查而不是特定的逻辑检查来更好地处理try-catch

  2. 如果您需要对异常执行某些操作(例如记录日志或回滚事务),请重新引发该异常。

  3. 始终尽可能高级别地处理未知异常- 应该使用异常而不重新抛出异常的唯一代码应该是UI或公共API。

假设您要连接到远程API,在这里您知道会遇到某些错误(在这种情况下会出错),因此是情况1:

try 
{
    remoteApi.Connect()
}
catch(ApiConnectionSecurityException ex) 
{
    // User's security details have expired
    return false;
}

return true;

请注意,没有其他异常会被捕获,这是不期望的。

现在假设您正在尝试将某些内容保存到数据库。如果失败,我们必须回滚它,所以我们有情况2:

try
{
    DBConnection.Save();
}
catch
{
    // Roll back the DB changes so they aren't corrupted on ANY exception
    DBConnection.Rollback();

    // Re-throw the exception, it's critical that the user knows that it failed to save
    throw;
}

请注意,我们重新引发了异常-更高级别的代码仍需要知道某些操作失败。

最后,我们有了UI-在这里我们不想拥有完全未处理的异常,但是我们也不想隐藏它们。这里有一个例子3:

try
{
    // Do something
}
catch(Exception ex) 
{
    // Log exception for developers
    WriteException2LogFile(ex);

    // Display message to users
    DisplayWarningBox("An error has occurred, please contact support!");
}

但是,大多数API或UI框架都具有执行案例3的通用方法。例如,ASP.Net具有黄色的错误屏幕,该屏幕上转储了异常详细信息,但是可以在生产环境中用更通用的消息替换。遵循这些最佳做法是因为它可以节省大量代码,而且因为错误日志记录和显示应该是配置决策,而不是硬编码。

这一切都意味着情况1(已知异常)和情况3(一次性UI处理)都具有更好的模式(避免将预期的错误或手工错误处理交给UI)。

即使情况2可以用更好的模式代替,例如,事务作用域using使该块回退该块中未提交的任何事务的块)也使开发人员更难于错误地获得最佳实践模式。

例如,假设您有一个大型ASP.Net应用程序。错误记录可以通过ELMAH进行,错误显示可以是本地的信息丰富的YSoD,也可以是生产中的本地化消息。数据库连接都可以通过事务作用域和using块进行。您不需要一个try-catch块。

TL; DR:最佳实践实际上是根本不使用try-catch块。


4
@Jorj,您应该阅读整篇文章,如果您仍然不同意,反驳我的一个支持论点可能会更具建设性,而不是仅仅声明您不喜欢我的结论。几乎总是有比try-catch这更好的模式-它可以(偶尔)有用,我并不是说您永远都不要使用它们,但是有99%的时间有更好的方法。
基思(Keith)

迄今为止最好的答案-几乎每种.net开发类型都具有某种类型的HANDLER,它更适合于在全局范围内处理异常,这使得更轻松地一致地处理它们以及使其更容易被吹走在开发中(为什么有人要通过日志文件来挖掘堆栈跟踪信息?一定可以捕获到特定的错误,但是将所有方法都包装在try / catch / log中非常疯狂
b_levitt 2015年

34

异常是阻塞错误

首先,最佳实践应该是不要为任何类型的错误抛出异常,除非它是阻塞错误

如果错误正在阻塞,则抛出异常。一旦引发了异常,就无需隐藏它,因为它是例外。让用户知道(您应将整个异常重新格式化为用户界面中对用户有用的内容)。

您作为软件开发人员的工作是努力避免某些情况下某些参数或运行时情况可能会异常终止的例外情况。也就是说,不得将异常静音,但必须避免这些异常

例如,如果您知道某些整数输入可能带有无效格式,请使用int.TryParse代替int.Parse。在许多情况下,您可以执行此操作,而不仅仅是说“如果失败,则抛出异常”。

抛出异常非常昂贵。

毕竟,如果引发了异常,而不是在引发异常后立即将异常写入日志,那么最佳实践之一就是将其捕获在优先机会处理程序中。例如:

  • ASP.NET:Global.asax中的Application_Error
  • 其他:AppDomain.FirstChanceException事件

我的立场是,本地try / catches更适合处理特殊情况,在特殊情况下,您可以将异常转换为另一个异常,或者在非常非常非常特殊的情况下想要对其进行“静音”(库错误)引发一个无关的异常,您需要将其静音才能解决整个错误)。

对于其余情况:

  • 尽量避免例外。
  • 如果这不可能:优先机会处理程序。
  • 或使用PostSharp方面(AOP)。

回答@thewhiteambit的一些评论...

@thewhiteambit说:

异常不是致命错误,它们是异常!有时它们甚至不是错误,但是将它们视为致命错误完全是对异常是什么的错误理解。

首先,一个异常怎么也不会是一个错误?

  • 没有数据库连接=>异常。
  • 无效的字符串格式解析为某种类型=>异常
  • 尝试解析JSON,而输入实际上不是JSON =>异常
  • null对象期望时的参数=>异常
  • 一些库有错误=>引发意外异常
  • 有套接字连接,但断开了连接。然后,您尝试发送一条消息=>异常
  • ...

我们可能会列出引发异常的一千种情况,毕竟任何可能的情况都是error

异常一个错误,因为在一天结束时,它是一个收集诊断信息的对象-它有一条消息,并且在出现问题时会发生。

在没有例外的情况下,没有人会抛出异常。异常应该阻止错误,因为一旦抛出异常,如果您不尝试使用try / catch和异常来实现控制流,则它们意味着您的应用程序/服务将停止发生异常情况的操作

另外,我建议大家检查一下马丁·福勒(Martin Fowler)(由吉姆·肖尔(Jim Shore)撰写)的“快速失败”范式。这就是我一直了解如何处理异常的方式,甚至是在我前一段时间获得本文档之前。

[...]认为它们是致命错误,完全是对异常的错误理解。

通常,异常会削减一些操作流程,并且将其处理为将其转换为人类可以理解的错误。因此,似乎异常实际上是处理错误案例并对其进行处理以避免应用程序/服务完全崩溃并通知用户/消费者发生问题的更好的范例。

有关@thewhiteambit关注的更多答案

例如,在缺少数据库连接的情况下,程序可以异常地继续写入本地文件,并将更改再次发送到数据库后再发送。您可以尝试使用Exception上的本地语言解释再次解析无效的String-To-Number强制转换,就像您尝试将默认英语语言解析为Parse(“ 1,5”)失败并再次使用德语解释尝试一样,这完全是完全正确的很好,因为我们使用逗号而不是点作为分隔符。您会看到这些异常甚至都不能被阻塞,它们只需要一些异常处理即可。

  1. 如果您的应用程序可以离线运行而无需将数据持久保存到数据库中,则不应使用exceptions,因为使用try/catch来实现控制流被视为一种反模式。离线工作是一个可能的用例,因此您实现控制流以检查数据库是否可访问,而不必等到数据库不可访问

  2. 分析事情也是预期的情况下(没有特殊情况)。如果您期望如此,则不要使用异常来执行控制流!。您从用户那里获取了一些元数据,以了解他/她的文化,然后使用格式化程序!.NET也支持此环境和其他环境,这是一个例外,因为如果您希望对应用程序/服务进行特定于文化的使用,则必须避免数字格式

未处理的异常通常会变成错误,但是异常本身不是codeproject.com/Articles/15921/Not-All-Exceptions-Are-Errors

本文仅是作者的一种观点或观点。

由于Wikipedia也可以是文章作者的观点,所以我不会说这是教条,而是检查“ 例外编码”一文在某段中的内容:

使用这些异常处理继续执行程序所发生的特定错误称为异常编码。这种反模式会迅速降低软件的性能和可维护性。

它还在某处说:

错误的异常用法

通常,异常编码会导致使用错误的异常用法导致软件出现其他问题。除了针对唯一问题使用异常处理之外,不正确的异常用法甚至在引发异常后也通过执行代码来进一步解决此问题。这种不良的编程方法类似于许多软件语言中的goto方法,但仅在检测到软件问题后才会发生。

老实说,我相信不能开发软件不会认真对待用例。如果你知道...

  • 您的数据库可以离线...
  • 某些文件可以被锁定...
  • 某些格式可能不受支持...
  • 某些域验证可能会失败...
  • 您的应用应在离线模式下运行...
  • 任何用例 ...

... 您不会为此使用例外。您将使用常规控制流来支持这些用例。

而且,如果未涵盖某些意外的用例,您的代码将很快失败,因为它将引发异常。是的,因为例外是例外情况

在另一方面,最后,有时你覆盖特殊情况下抛出预期的异常,但你不把它们实施控制流。这样做是因为您想通知高层您不支持某些用例,或者您的代码无法使用某些给定的参数或环境数据/属性。


6

您唯一应该担心用户的代码中发生的事情的情况是,他们是否可以或需要采取某些措施来避免该问题。如果他们可以更改表单上的数据,请按下按钮或更改应用程序设置,以避免出现此问题,然后让他们知道。但是用户无法避免的警告或错误只会使他们对您的产品失去信心。

例外和日志适用于您(开发人员),而不是您的最终用户。理解捕获每个异常时的正确做法远比仅应用一些黄金法则或依赖整个应用程序的安全网要好得多。

无心编码是唯一一种错误编码。您认为在这些情况下可以做的更好的事实表明您已投入了良好的编码,但避免尝试在这些情况下标记一些通用规则,并了解首先要扔出东西的原因和原因您可以从中恢复。


6

我知道这是一个老问题,但是这里没有人提到MSDN文章,而实际上是为我清除了该文档,MSDN对此有很好的文档,当满足以下条件时,您应该捕获异常:

  • 您对可能引发异常的原因有了很好的了解,并且可以实现特定的恢复,例如在捕获FileNotFoundException对象时提示用户输入新文件名。

  • 您可以创建并引发一个新的,更具体的异常。

int GetInt(int[] array, int index)
{
    try
    {
        return array[index];
    }
    catch(System.IndexOutOfRangeException e)
    {
        throw new System.ArgumentOutOfRangeException(
            "Parameter index is out of range.");
    }
}
  • 您需要先处理异常,然后再将其传递给其他处理。在以下示例中,在重新引发异常之前,使用catch块将条目添加到错误日志中。
    try
{
    // Try to access a resource.
}
catch (System.UnauthorizedAccessException e)
{
    // Call a custom error logging procedure.
    LogError(e);
    // Re-throw the error.
    throw;     
}

我建议阅读整个“ 异常和异常处理 ”部分以及有关异常的最佳实践


1

更好的方法是第二种(您可以在其中指定异常类型的方法)。这样做的好处是您知道这种类型的异常可以在代码中发生。您正在处理这种类型的异常,您可以继续。如果出现任何其他异常,则意味着出了点问题,可以帮助您在代码中查找错误。该应用程序最终将崩溃,但是您将了解到某些遗漏(错误)需要修复的问题。


1

对于“例外”,我尝试以下操作:

首先,我捕获特殊类型的异常,例如被零除,IO操作等等,并根据这些异常编写代码。例如,除以零,取决于值的出处,我可以警告用户(例如,一个简单的计算器,在中间计算中(不是参数)到达除以零)或静默处理该异常,记录并继续处理。

然后,我尝试捕获其余的异常并将其记录下来。如果可能,允许执行代码,否则警告用户发生了错误,并要求他们邮寄错误报告。

在代码中,如下所示:

try{
    //Some code here
}
catch(DivideByZeroException dz){
    AlerUserDivideByZerohappened();
}
catch(Exception e){
    treatGeneralException(e);
}
finally{
    //if a IO operation here i close the hanging handlers for example
}

1
除以零例外之类的方法时,最好0事先检查分子而不是try-catch。还为什么在Exception这里抓住通用的?与不希望在所有情况下在此处处理错误相比,让错误冒出来更好。
基思

更好地阅读我所写的关于示例的内容-注意“不在参数中”。当然,任何计算器都应验证给定的参数。我所说的是中间步骤。那时,用户参数验证已经发生。同样在某些应用程序中,最好避免出现异常。一些应用程序应静默处理异常,而其他应用程序应将异常视为错误。例如,即使发生异常,Web服务器也应运行,而发生异常时,医疗软件(例如X射线机)应中止。
Sorcerer86pt

没有应用程序应该永远把例外默默。有时您会有一个代码可以处理的异常,但是这种用法应该很少而且特定于预期的异常。您的网络服务器示例很差劲-它应该具有配置设置,使您可以选择记录错误的方式以及是否详细显示错误或仅显示HTTP 500页面,但是它们绝不能默默地忽略错误。
基思

我试图弄清楚是什么真正促使人们去添加“ goto”的同义词。但是就零除而言,这将是一种例外,我可以看到它证明了语言增强的合理性。为什么?因为很可能是这样的情况:A)零在统计上是数据集中的一个无穷小,而B)使用(允许)例外可能会更有效,因为进行除法是测试零除数的一种方法。当A和B为true时,使用例外情况,程序的平均执行速度会更快,甚至可能更小。
Mike Layton

1

第二种方法是一种好的方法。

如果您不想显示错误并通过显示与错误无关的运行时异常(即错误)来使应用程序的用户迷惑,则只需记录错误,技术团队便可以查找问题并解决。

try
{
  //do some work
}
catch(Exception exception)
{
   WriteException2LogFile(exception);//it will write the or log the error in a text file
}

我建议您为整个应用程序选择第二种方法。


2
第二种方法不向用户显示发生了错误-例如,如果他们正在保存某些内容,他们将不知道它已失败。catch块应该始终调用throw以使异常冒泡,或者返回一些内容/显示一些内容以告诉用户操作已失败。您希望在他们无法保存任何内容时获得支持电话,而不是6个月后他们尝试检索并找不到它时获得支持电话。
基思

0

留空捕获块是最糟糕的事情。如果有错误,最好的解决方法是:

  1. 将其登录到文件\数据库等。
  2. 尝试即时修复它(也许尝试执行该操作的另一种方法)
  3. 如果我们无法解决该问题,请通知用户有错误,然后中止操作

0

对我来说,处理异常可以看作是业务规则。显然,第一种方法是不可接受的。第二个比较好,如果上下文这样说的话,这可能是100%正确的方式。现在,例如,您正在开发Outlook插件。如果您的插件引发未处理的异常,则Outlook用户现在可能知道该异常,因为由于一个插件失败,Outlook不会自行销毁。而且您很难找出问题所在。因此,对我而言,在这种情况下的第二种方法是正确的。除了记录异常外,您可能决定向用户显示错误消息-我认为这是业务规则。


0

最佳实践是在发生错误时引发Exception。因为发生了错误,所以不应将其隐藏。

但是在现实生活中,当您想要隐藏此内容时,可能会遇到几种情况

  1. 您依赖第三方组件,并且在出现错误的情况下想要继续执行该程序。
  2. 您有一个业务案例,需要在发生错误的情况下继续进行

6
难道Exception曾经 抛出Exception您想要的所有子类的适当子类,但决不要Exception因为它绝对不提供语义信息。我毫不费力地看不到一个有意义的场景,Exception但不是它的子类。
CVn


0

catch不带任何参数仅仅是的例外,是没有用的。如果发生致命错误怎么办?如果不带参数使用catch,就无法知道会发生什么。

catch语句应捕获更具体的异常,例如FileNotFoundException,然后最后应捕获Exception将捕获任何其他异常的日志并将其记录下来。


为什么最后有将军catch(Exception)?如果您不期望这样做,则始终最好的做法是将其传递到下一层。
基思

1
@Keith是的,您是正确的...捕获意外的异常没有任何意义,但是您可以出于记录目的而使用一般异常
。– Anirudha

0

有时您需要处理对用户无能为力的异常。

我的方式是:

  • 在应用程序级别(例如,global.asax中)捕获未捕获的异常以获取关键异常(应用程序无用)。我无法掌握这些技巧。只需在应用程序级别上登录它们,然后让系统执行其工作即可。
  • 赶上“就地”并向用户显示一些有用的信息(输入错误的数字,无法解析)。
  • 赶上就地,对诸如“我将在后台检查更新信息,但服务未运行”之类的边际问题不采取任何措施。

绝对不必是最佳实践。;-)


0

我可以告诉你一些事情:

片段1是不能接受的,因为它忽略了异常。(它吞没了它,就像什么也没有发生一样)。

因此,请勿添加什么也不做或只是重新抛出的catch块。

捕获块应添加一些值。例如,输出消息给最终用户或记录错误。

请勿将异常用于常规流程序逻辑。例如:

例如输入验证。<-这不是有效的例外情况,而是您应该编写方法IsValid(myInput);来检查输入项是否有效。

设计代码以避免异常。例如:

int Parse(string input);

如果我们将无法解析的值传递给int,则此方法将引发异常,相反,我们可能会编写如下内容:

bool TryParse(string input,out int result); <-此方法将返回布尔值,指示解析是否成功。

也许这有点超出这个问题的范围,但是我希望这将帮助您在即将发生的事情try {} catch(){}和例外情况下做出正确的决定。

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.