捕获和重新抛出 .NET 异常的最佳实践

Posted

技术标签:

【中文标题】捕获和重新抛出 .NET 异常的最佳实践【英文标题】:Best practices for catching and re-throwing .NET exceptions 【发布时间】:2010-09-06 13:29:45 【问题描述】:

在捕获异常并重新抛出异常时要考虑哪些最佳做法?我想确保保留 Exception 对象的 InnerException 和堆栈跟踪。以下代码块的处理方式有区别吗?

try

    //some code

catch (Exception ex)

    throw ex;

对比:

try

    //some code

catch

    throw;

【问题讨论】:

【参考方案1】:

保留堆栈跟踪的方法是使用throw; 这也是有效的

try 
  // something that bombs here
 catch (Exception ex)

    throw;

throw ex; 基本上就像从那时开始抛出异常,因此堆栈跟踪只会转到您发出 throw ex; 语句的位置。

Mike 也是正确的,假设异常允许你传递异常(这是推荐的)。

Karl Seguin 在他的foundations of programming e-book 中也有一个great write up on exception handling,非常适合阅读。

编辑:Foundations of Programming pdf 的工作链接。只需在文本中搜索“异常”即可。

【讨论】:

我不太确定这篇文章是否精彩,它建议尝试 // ... catch(Exception ex) throw new Exception(ex.Message + "other stuff" ); 很好。问题是您完全无法在堆栈中进一步处理该异常,除非您捕获所有异常,这是一个很大的禁忌(您确定要处理该 OutOfMemoryException 吗?) @ljs 自从您发表评论以来,文章是否已更改,因为我没有看到他推荐的任何部分。事实上恰恰相反,他说不要这样做,并问你是否也想处理 OutOfMemoryException!? 有时 throw; 不足以保留堆栈跟踪。这是一个例子https://dotnetfiddle.net/CkMFoX ExceptionDispatchInfo.Capture(ex).Throw(); throw; in .NET +4.5 ***.com/questions/57383/… 这个答案已经过时了。 here's why it is bad。 Here is the correct answer in .NET 4.5 and later【参考方案2】:

如果你抛出一个带有初始异常的新异常,你也将保留初始堆栈跟踪。

try
 
catch(Exception ex)
     throw new MoreDescriptiveException("here is what was happening", ex);

【讨论】:

无论我尝试什么 throw new Exception("message", ex) 总是抛出 ex 并忽略自定义消息。 throw new Exception("message", ex.InnerException) 虽然有效。 如果不需要自定义异常,可以使用 AggregateException (.NET 4+) msdn.microsoft.com/en-us/library/… AggregateException 应该只用于聚合操作的异常。例如,它由 CLR 的 ParallelEnumerableTask 类抛出。用法大概应该遵循这个例子。【参考方案3】:

实际上,throw 语句在某些情况下不会保留 StackTrace 信息。例如,在下面的代码中:

try

  int i = 0;
  int j = 12 / i; // Line 47
  int k = j + 1;

catch

  // do something
  // ...
  throw; // Line 54

StackTrace 将表明第 54 行引发了异常,尽管它是在第 47 行引发的。

Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
   at Program.WithThrowIncomplete() in Program.cs:line 54
   at Program.Main(String[] args) in Program.cs:line 106

在上述情况下,有两个选项可以保留原始 StackTrace:

调用 Exception.InternalPreserveStackTrace

由于是私有方法,所以必须使用反射来调用:

private static void PreserveStackTrace(Exception exception)

  MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace",
    BindingFlags.Instance | BindingFlags.NonPublic);
  preserveStackTrace.Invoke(exception, null);

我的缺点是依赖私有方法来保存 StackTrace 信息。它可以在 .NET Framework 的未来版本中进行更改。上面的代码示例和下面提出的解决方案摘自Fabrice MARGUERIE weblog。

调用 Exception.SetObjectData

Anton Tykhyy 建议使用以下技术作为对In C#, how can I rethrow InnerException without losing stack trace 问题的回答。

static void PreserveStackTrace (Exception e) 
 
  var ctx = new StreamingContext  (StreamingContextStates.CrossAppDomain) ; 
  var mgr = new ObjectManager     (null, ctx) ; 
  var si  = new SerializationInfo (e.GetType (), new FormatterConverter ()) ; 

  e.GetObjectData    (si, ctx)  ; 
  mgr.RegisterObject (e, 1, si) ; // prepare for SetObjectData 
  mgr.DoFixups       ()         ; // ObjectManager calls SetObjectData 

  // voila, e is unmodified save for _remoteStackTraceString 
 

虽然它具有仅依赖于公共方法的优点,但它还依赖于以下异常构造函数(第三方开发的某些异常未实现):

protected Exception(
    SerializationInfo info,
    StreamingContext context
)

在我的情况下,我不得不选择第一种方法,因为我使用的第 3 方库引发的异常没有实现这个构造函数。

【讨论】:

您可以捕获异常并将此异常发布到您想要的任何地方。然后抛出一个新的解释发生在用户身上的事情。这样你可以看到当前异常被捕获时发生了什么,用户可以忽略实际异常是什么。 在 .NET 4.5 中还有第三个——在我看来——更简洁的选项:使用 ExceptionDispatchInfo。请参阅 Tragedians 对相关问题的回答:***.com/a/17091351/567000 了解更多信息。 这里一个普通的throw; 显示第 54 行而不是第 47 行可能应该被认为是 .NET Framework 中的一个长期存在的错误,并在 .NET Core 2.1 (github.com/dotnet /runtime/issues/9518)。您可以使用ExceptionDispatchInfo,但这不是它的主要用例之一(***.com/a/17091351 中显示了其中一个),这表明它会使水变得混乱并导致代码可读性降低。也就是说,从 catch 中获取行,以及调用堆栈中的任何其他行号对我来说已经足够了。【参考方案4】:

当您throw ex 时,您实际上是在抛出一个新异常,并且会错过原始堆栈跟踪信息。 throw 是首选方法。

【讨论】:

【参考方案5】:

经验法则是避免捕获和抛出基本的Exception 对象。这迫使你对异常更聪明一点;换句话说,您应该明确地捕获 SqlException,这样您的处理代码就不会对 NullReferenceException 造成错误。

不过,在现实世界中,捕获并记录基本异常也是一种好习惯,但不要忘记遍历整个过程以获取它可能拥有的任何InnerExceptions。 p>

【讨论】:

我认为最好使用 AppDomain.CurrentDomain.UnhandledException 和 Application.ThreadException 异常来处理未处理的异常以进行日志记录。到处使用 big try ... catch(Exception ex) ... 块意味着很多重复。取决于您是否要记录已处理的异常,在这种情况下(至少最少)重复可能是不可避免的。 加上使用这些事件意味着你记录所有未处理的异常,而如果你使用 big ol' try ... catch(Exception ex) ... 你可能会错过一些块。【参考方案6】:

没有人解释过ExceptionDispatchInfo.Capture( ex ).Throw() 和普通的throw 之间的区别,所以在这里。不过也有人注意到throw的问题。

重新抛出捕获的异常的完整方法是使用ExceptionDispatchInfo.Capture( ex ).Throw()(仅适用于 .Net 4.5)。

下面是测试这个的必要案例:

1.

void CallingMethod()

    //try
    
        throw new Exception( "TEST" );
    
    //catch
    
    //    throw;
    

2.

void CallingMethod()

    try
    
        throw new Exception( "TEST" );
    
    catch( Exception ex )
    
        ExceptionDispatchInfo.Capture( ex ).Throw();
        throw; // So the compiler doesn't complain about methods which don't either return or throw.
    

3.

void CallingMethod()

    try
    
        throw new Exception( "TEST" );
    
    catch
    
        throw;
    

4.

void CallingMethod()

    try
    
        throw new Exception( "TEST" );
    
    catch( Exception ex )
    
        throw new Exception( "RETHROW", ex );
    

案例 1 和案例 2 将为您提供堆栈跟踪,其中 CallingMethod 方法的源代码行号是 throw new Exception( "TEST" ) 行的行号。

但是,案例 3 将为您提供堆栈跟踪,其中 CallingMethod 方法的源代码行号是 throw 调用的行号。这意味着如果throw new Exception( "TEST" ) 行被其他操作包围,您将不知道实际抛出异常的行号。

情况 4 与情况 2 类似,因为保留了原始异常的行号,但不是真正的重新抛出,因为它改变了原始异常的类型。

【讨论】:

添加一个简单的简介,永远不要使用throw ex;,这是最好的答案。【参考方案7】:

您应该始终使用“throw;”在 .NET 中重新抛出异常,

参考这个, http://weblogs.asp.net/bhouse/archive/2004/11/30/272297.aspx

基本上MSIL(CIL)有两条指令——“throw”和“rethrow”:

C# 的“throw ex;”被编译成 MSIL 的“抛出” C# 的“投掷”; - 进入 MSIL “重新投掷”!

基本上我可以看到“throw ex”覆盖堆栈跟踪的原因。

【讨论】:

链接——嗯,实际上the source that link cites——充满了很好的信息,并且还指出了为什么许多人认为throw ex;会重新抛出的一个可能的罪魁祸首——在Java中,确实如此!但是您应该在此处包含该信息以获得 A 级答案。 (虽然我还在赶上ExceptionDispatchInfo.Capture answer from jeuoekdcwzfwccu。)【参考方案8】:

有些人实际上错过了一个非常重要的点 - 'throw' 和 'throw ex' 可能会做同样的事情,但他们没有给你一个关键的信息,即异常发生的地方。

考虑以下代码:

static void Main(string[] args)

    try
    
        TestMe();
    
    catch (Exception ex)
    
        string ss = ex.ToString();
    


static void TestMe()

    try
    
        //here's some code that will generate an exception - line #17
    
    catch (Exception ex)
    
        //throw new ApplicationException(ex.ToString());
        throw ex; // line# 22
    

当您执行“throw”或“throw ex”时,您会得到堆栈跟踪,但 line# 将是 #22,因此您无法确定究竟是哪一行引发了异常(除非您只有try 块中的 1 行或几行代码)。要在异常中获得预期的第 17 行,您必须使用原始异常堆栈跟踪抛出一个新异常。

【讨论】:

这里一个普通的throw; 也显示了第 22 行而不是第 17 行,这应该被认为是 .NET Framework 中的一个长期存在的错误,并在 .NET Core 2.1 中得到了修复 (github.com/dotnet/runtime/issues/9518) .也就是说,我从来没有见过我关心在堆栈跟踪中看到来自try 的行的实例。从 catch 中获取行,以及调用堆栈中的任何其他行号总是足够好的。【参考方案9】:

你也可以使用:

try

// Dangerous code

finally

// clean up, or do nothing

并且抛出的任何异常都会冒泡到处理它们的下一个级别。

【讨论】:

【参考方案10】:

我肯定会使用:

try

    //some code

catch

    //you should totally do something here, but feel free to rethrow
    //if you need to send the exception up the stack.
    throw;

这将保留您的堆栈。

【讨论】:

为了公平起见,在 2008 年,OP 在询问如何保留堆栈——而 2008 年我给出了正确的答案。我的回答中缺少的是实际做某事的部分。 @JohnSaunders 当且仅当您在throw 之前做任何事情时才是正确的;例如,您可以清理一次性(仅在出现错误时调用它),然后抛出异常。 @meirion 当我写评论时,在投掷之前什么都没有。添加后,我投了赞成票,但没有删除评论。【参考方案11】:

仅供参考,我刚刚测试了这个和'throw;'报告的堆栈跟踪不是完全正确的堆栈跟踪。示例:

    private void foo()
    
        try
        
            bar(3);
            bar(2);
            bar(1);
            bar(0);
        
        catch(DivideByZeroException)
        
            //log message and rethrow...
            throw;
        
    

    private void bar(int b)
    
        int a = 1;
        int c = a/b;  // Generate divide by zero exception.
    

堆栈跟踪正确地指向异常的来源(报告的行号),但为 foo() 报告的行号是抛出的行;声明,因此您无法判断对 bar() 的哪些调用导致了异常。

【讨论】:

这就是为什么最好不要尝试捕获异常,除非你打算用它们做点什么

以上是关于捕获和重新抛出 .NET 异常的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

Java 处理异常 9 个最佳实践

定义自己的异常类的最佳实践?

在线程中处理在 catch 块中抛出的异常的最佳实践。 (。网)

异步编程最佳实践

最佳实践:从属性中抛出异常

Java异常处理和最佳实践(含案例分析)