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

Posted

技术标签:

【中文标题】如何使用 try catch 进行异常处理是最佳实践【英文标题】:How using try catch for exception handling is best practice 【发布时间】:2013-02-05 02:06:22 【问题描述】:

在维护我同事的代码时,即使是自称是高级开发人员的人,我也经常看到以下代码:

try

  //do something

catch

  //Do nothing

或者有时他们会将日志信息写入日志文件,例如关注try catch

try

  //do some work

catch(Exception exception)

   WriteException2LogFile(exception);

我只是想知道他们所做的是否是最佳实践?这让我很困惑,因为在我看来,用户应该知道系统会发生什么。

【问题讨论】:

片段 #1 在 99.999% 的情况下是不可接受的。 直接向用户显示异常从来都不是一个好主意,主要有两个原因: 1. 如果是普通用户,他会厌烦阅读对他/她来说很少的错误消息。 2. 如果他是所谓的黑客,他可能会得到有用的信息。 IMO 的最佳做法是记录异常并显示友好的错误消息。 @leppie 如果发生意外情况(例如 NullReferenceArgumentNull 这不是应用程序流程的一部分),则意味着存在需要修复的错误,因此记录它们将有助于调试您的编码速度更快。 使用 try-catch 块隐藏异常通常是惰性编程的结果。这是一种常用的快捷方式,而不是编写验证代码来测试输入。有时可能会出现不影响代码操作的异常,并且像这样隐藏它可能是可以的。然而,这种情况相当罕见。 @Toan,好吧,如果它是一个批处理作业,我会在 top level (Main) 捕获记录,然后重新抛出以触发警报作业异常终止。如果它是一个网络应用程序,我会让异常冒泡到全局处理程序,记录,然后将用户重定向到错误屏幕。您的用例场景决定了您在记录或以其他方式处理该异常后如何处理它。 【参考方案1】:

我的异常处理策略是:

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

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

然后我总是将在外部运行的每一段代码包含在try/catch 中:

WinForms 基础结构触发的所有事件(Load、Click、SelectedChanged...) 第三方组件触发的所有事件

然后我用'try/catch'括起来

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

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

需要立即显示给用户

当它们发生时需要一些额外的处理来将它们放在一起以避免级联问题(即:在TreeView 填充期间将.EndUpdate 放入finally 部分)

用户并不关心,但重要的是要知道发生了什么。所以我总是记录它们:

在事件日志中

或在磁盘上的 .log 文件中

在应用程序***错误处理程序中设计一些静态方法来处理异常是一种很好的做法。

我也强迫自己尝试:

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

最后:

不好:

// DON'T DO THIS; ITS BAD
try

    ...

catch 

   // only air...

没用的:

// DON'T DO THIS; IT'S USELESS
try

    ...

catch(Exception ex)

    throw ex;

在没有捕获的情况下尝试 finally 是完全有效的:

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 OCCURRED 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;

【讨论】:

C# 中的 catch(Exception ex) throw ex; 比冗余(无论您捕获的异常类型如何)worse。要重新抛出,请使用throw;。对于前者,异常看起来像是源自您的throw ex,而对于后者,它将正确源自原始throw 语句。 为什么要挂钩Application.ThreadException 事件catch(Exception ex) ex.Log(ex); 包装每个异常。我可能会同意前者是一种很好的做法,但后者增加了复制错误日志的风险并隐藏了发生异常的情况。还有throw ex 非常非常糟糕。 我了解 catch(Exception ex) throw ex; 没用。所以我认为“冗余”并不是说“不要这样做”的最佳词。这就是为什么我稍微更改了帖子以更好地说明必须避免第一个 try catch 示例。 伟大而有建设性的答案,最重要的是我喜欢这句话 Only air :) 感谢Application.ThreadException 活动,我不知道这一点,非常有用. MSDN 建议 You should not throw an ApplicationException exception in your code.【参考方案2】:

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

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

    始终尽可能低调地处理已知异常。但是,如果您预计会出现异常,通常最好先对其进行测试。例如,解析、格式化和算术异常几乎总是首先通过逻辑检查来更好地处理,而不是特定的try-catch

    如果您需要对异常执行某些操作(例如记录或回滚事务),则重新抛出异常。

    总是尽可能地处理 unknown 异常 - only 代码应该使用异常而不是重新抛出它应该是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 也可以用更好的模式代替,例如 transaction scopes(using 块会回滚在块期间未提交的任何事务)使开发人员更难将最佳实践模式弄错。

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

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

【讨论】:

@Jorj 你应该阅读整篇文章,如果你仍然不同意,也许反驳我的一个支持论点会更有建设性,而不是仅仅说你不喜欢我的结论。几乎总是有比try-catch 更好的模式——它可能(非常偶尔)有用,我并不是说你永远不应该使用它们,但 99% 的时间有更好的方法。 迄今为止最好的答案 - 几乎每个 .net 开发类型都有某种更适合在全局级别处理异常的处理程序,从而更容易一致地处理它们以及使简单地让它在开发中爆炸要容易得多(为什么有人想通过日志文件挖掘堆栈跟踪??)@Kieth,我会以你的 TLDR 为首,并添加一些全局处理程序的示例(即ThreadException、Application_Error 等)。无论如何要捕获特定的错误,但是将任何方法包装在 try/catch/log 中是很疯狂的【参考方案3】:

异常是阻塞错误

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

如果错误是阻塞,则抛出异常。一旦异常已经抛出,就不需要隐藏它,因为它是异常的;让用户知道它(您应该在 UI 中将整个异常重新格式化为对用户有用的内容)。

作为软件开发人员,您的工作是努力防止出现异常情况,其中某些参数或运行时情况可能以异常结束。也就是说,不能忽略异常,但必须避免这些异常

例如,如果您知道某些整数 输入可能带有无效格式,请使用int.TryParse 而不是int.Parse。在很多情况下,您可以这样做,而不仅仅是说“如果失败,只需抛出异常”。

抛出异常是昂贵的。

毕竟,如果抛出异常,与其在抛出异常后将异常写入日志,最佳实践之一是在第一次机会异常处理程序中捕获它。例如:

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

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

对于其余情况:

尽量避免异常。 如果这不可能:第一次机会异常处理程序。 或使用 PostSharp aspect (AOP)。

就一些评论回复@thewhiteambit...

@thewhiteambit 说:

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

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

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

我们可能会列出 1k 种抛出异常的情况,毕竟,任何可能的情况都将是错误

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

当没有异常情况时,没有人会抛出异常。异常应该是阻塞错误,因为一旦它们被抛出,如果你不尝试陷入使用 try/catch 和异常来实现控制流,它们就意味着你的应用程序/service 将停止进入异常情况的操作。

另外,我建议大家检查 fail-fast 范例published by Martin Fowler (and written by Jim Shore)。这就是我一直了解如何处理异常的方式,甚至在我前段时间阅读本文档之前。

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

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

关于@thewhiteambit 问题的更多答案

例如,在缺少数据库连接的情况下,程序可以 异常继续写入本地文件并发送 再次可用时更改数据库。你的无效 可以尝试再次解析字符串到数字的转换 异常上的本地语言解释,就像您尝试默认一样 英语解析(“1,5”)失败,你用德语试试 再次解释这完全没问题,因为我们使用逗号 而不是点作为分隔符。你看这些 Exceptions 一定不能 被阻塞,他们只需要一些异常处理。

    如果您的应用可能在不将数据持久化到数据库的情况下离线工作,则不应使用异常,因为使用try/catch 实现控制流被视为反模式。 离线工作是一个可能的用例,因此您实施控制流来检查数据库是否可访问,您不要等到它无法访问

    解析也是一种预期的情况(不是例外情况)。如果您期望这一点,您不要使用异常来进行控制流!。您从用户那里获得一些元数据以了解他/她的文化,并为此使用格式化程序! .NET 也支持这种环境和其他环境,但如果您希望应用程序/服务具有特定于文化的用法,则必须避免使用数字格式

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

本文只是作者的观点或观点。

由于***也可能只是文章作者的观点,我不会说这是教条,但请查看Coding by exception 文章在某段某处所说的内容:

[...] 使用这些异常来处理出现的特定错误 继续程序被称为异常编码。 这种反模式会迅速降低软件的性能和可维护性。

它还在某处说:

异常用法不正确

通常按异常进行编码会导致软件出现更多问题 异常使用不正确。除了使用异常 处理一个独特的问题,不正确的异常使用需要这个 即使在引发异常之后也执行代码。这 糟糕的编程方法类似于许多软件中的 goto 方法 语言,但仅在检测到软件问题后才会发生。

老实说,我认为如果不认真对待用例,就无法开发软件。如果你知道...

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

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

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

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

【讨论】:

【参考方案4】:

您唯一应该让用户担心代码中发生的事情是他们可以或需要做些什么来避免该问题。如果他们可以更改表单上的数据、按下按钮或更改应用程序设置以避免问题,请让他们知道。但是用户无法避免的警告或错误只会让他们对你的产品失去信心。

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

无意识的编码是唯一一种错误的编码。你觉得在这些情况下可以做一些更好的事情,这表明你对良好的编码进行了投资,但避免试图在这些情况下标记一些通用规则,并首先了解要抛出某些东西的原因以及什么你可以从中恢复过来。

【讨论】:

【参考方案5】:

我知道这是一个老问题,但是这里没有人提到 MSDN 文章,而且是该文档实际上为我清理了它,MSDN 对此有一个very good document,您应该在以下情况下捕获异常真的:

您对可能抛出异常的原因有了很好的理解,并且可以实现特定的恢复,例如在捕获 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;     

我建议阅读整个“Exceptions and Exception Handling”部分以及Best Practices for Exceptions。

【讨论】:

【参考方案6】:

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

【讨论】:

【参考方案7】:

如果有例外,我会尝试以下方法:

首先,我捕获特殊类型的异常,例如被零除、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

【讨论】:

除以零异常等问题最好通过事先检查0 分子而不是try-catch 来处理。另外为什么要在这里捕获通用Exception?你最好让错误冒泡而不是在所有你不期望的情况下在这里处理它。 更好地阅读我所写的关于我给出的示例的内容——注意“不在参数中”。当然,任何计算器都应该验证给定的参数。我谈的是中间步骤。那时,用户参数验证已经发生。此外,在某些应用程序中,最好避免出现异常。一些应用程序应该静默处理异常,而其他应用程序应该将异常视为错误。例如,即使发生异常,Web 服务器也应该运行,而医疗软件(例如 X 射线机)应该在发生异常时中止。 没有应用程序应该永远静默处理异常。有时您会遇到代码可以处理的异常,但这种用法应该既罕见又特定于预期的异常。您的 Web 服务器示例很差 - 它应该具有配置设置,让您可以选择如何记录错误以及是否显示详细信息或仅显示 HTTP 500 页面,但它们应该从不默默地忽略错误。 我试图弄清楚是什么真正促使人们再添加一个“goto”的同义词。但是关于除以零,这将是我可以看到的一种类型的异常,它证明了语言增强是合理的。为什么?因为很可能 A)零在您的数据集中在统计上是一个无穷小,并且 B)使用(允许)异常可能更有效,因为进行除法是测试零除数的一种方法。当 A 和 B 为真时,使用异常的程序的平均执行速度会更快,甚至可能更小。【参考方案8】:

第二种方法很好。

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

try

  //do some work

catch(Exception exception)

   WriteException2LogFile(exception);//it will write the or log the error in a text file

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

【讨论】:

第二种方法不会向用户显示发生了错误 - 例如,如果他们正在保存某些内容,他们不会知道它已经失败。 catch 块应该总是调用throw 来冒泡异常,或者返回一些东西/显示一些东西来告诉用户操作失败了。您希望在他们无法保存任何内容时接到支持电话,而不是在 6 个月后当他们尝试检索它但找不到它时。【参考方案9】:

留空 catch 块是最糟糕的事情。如果出现错误,最好的处理方法是:

    将其登录到文件\数据库等中。 尝试即时修复它(也许尝试执行该操作的替代方法) 如果我们无法解决此问题,请通知用户存在一些错误,当然要中止操作

【讨论】:

【参考方案10】:

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

【讨论】:

【参考方案11】:

最佳做法是在发生错误时抛出异常。因为发生了错误,不应该被隐藏。

但在现实生活中,您可能有多种情况想要隐藏它

    您依赖第三方组件并希望在出现错误时继续执行程序。 您有一个业务案例需要在出现错误时继续处理

【讨论】:

不。不要不要抛出ExceptionEver. 随心所欲地抛出Exception 的适当子类,但永远不要抛出Exception,因为这绝对不会提供任何语义信息。我完全看不到抛出 Exception 而不是其子类的情况。【参考方案12】:

您应该考虑这些例外设计指南

异常抛出 使用标准异常类型 异常和性能

https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/exceptions

【讨论】:

【参考方案13】:

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

catch 语句应该捕获更多特定异常,例如FileNotFoundException,然后在结束你应该捕获Exception,它会捕获任何其他异常并记录它们.

【讨论】:

为什么最后有一个通用的catch(Exception)?如果您不期待它,那么最好将其传递到下一层。 @Keith 是的,你是对的......捕获你没有预料到的异常是没有意义的,但你可以将一般异常用于记录目的......【参考方案14】:

有时您需要处理对用户无意义的异常。

我的方法是:

在应用程序级别(即在 global.asax 中)捕获关键异常(应用程序无法使用)的未捕获异常。这些例外我没有赶上这个地方。只需在应用级别记录它们,然后让系统完成其工作。 “就地”捕捉并向用户显示一些有用的信息(输入错误的数字,无法解析)。 抓紧时间,对诸如“我将在后台检查更新信息,但服务未运行”等边缘问题不采取任何措施。

这绝对不一定是最佳实践。 ;-)

【讨论】:

【参考方案15】:

我可以告诉你一些事情:

片段 #1 是不可接受的,因为它忽略了异常。 (它像什么都没发生一样吞下它)。

所以不要添加什么都不做或只是重新抛出的 catch 块。

Catch 块应该增加一些价值。例如向最终用户输出消息或记录错误。

不要对正常流程程序逻辑使用异常。例如:

例如输入验证。 IsValid(myInput); 来检查输入项是否有效。

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

int Parse(string input);

如果我们将无法解析的值传递给 int,此方法将抛出异常,而不是我们可能会编写如下内容:

bool TryParse(string input,out int result);

也许这有点超出了这个问题的范围,但我希望这将有助于您在关于 try catch() 和例外情况时做出正确的决定。

【讨论】:

以上是关于如何使用 try catch 进行异常处理是最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

JAVA语言如何进行异常处理,关键字:throws,throw,try,catch,finally分别代表啥意

JAVA 语言如何进行异常处理,关键字: throws,throw,try,catch,finally分别代表什么意义? 在try块中可以抛 出异常吗?

Java面试题22 JAVA语言如何进行异常处理,关键字:throws,throw,try,catch,finally分别代表什么意义?在try块中可以抛出异常吗?

如何使用多个catch块处理异常

JAVA语言如何进行异常处理,关键字:throws,throw,try,catch,finally分别代表啥意义?

try-catch的使用以及细节