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


284

捕获异常并重新抛出异常时应考虑哪些最佳实践?我想确保保留Exception对象的InnerException和堆栈的跟踪信息。以下代码块之间的处理方式是否有所不同?

try
{
    //some code
}
catch (Exception ex)
{
    throw ex;
}

VS:

try
{
    //some code
}
catch
{
    throw;
}

Answers:


262

保存堆栈跟踪的方法是通过使用的throw;,这是合法以及

try {
  // something that bombs here
} catch (Exception ex)
{
    throw;
}

throw ex;从本质上讲,就像从该点引发异常一样,因此堆栈跟踪只会到达您发出该throw ex;语句的位置。

迈克也是正确的,假设该异常允许您传递异常(建议)。

Karl Seguin编程电子书的基础上也对异常处理进行了出色的撰写,这是一本不错的书。

编辑:工作链接到编程基础 pdf。只需在文本中搜索“ exception”即可。


10
我不确定这种写法是否很棒,建议您尝试{// ...} catch(Exception ex){throw new Exception(ex.Message +“ other something”); } 很好。问题在于,除非您捕获了所有异常,否则您将无法完全在堆栈上进一步处理该异常,这是一个很大的
禁忌

2
@ljs自您发表评论以来,文章是否已更改,因为我没有看到他建议的任何部分。实际上,情况恰恰相反,他说不这样做,并询问您是否也要处理OutOfMemoryException !?
RyanfaeScotland,2015年

6
有时抛出;不足以保留堆栈跟踪。这是一个示例https://dotnetfiddle.net/CkMFoX
Artavazd Balayan

4
ExceptionDispatchInfo.Capture(ex).Throw(); throw;在.NET +4.5stackoverflow.com/questions/ 57383/…
阿尔弗雷德·华莱士

@AlfredWallace解决方案非常适合我。尝试{...} catch {throw}没有保留堆栈跟踪。谢谢。
atownson

100

如果您使用初始异常引发新的异常,则您还将保留初始堆栈跟踪。

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

不管我尝试抛出什么,新的Exception(“ message”,ex)总是抛出ex并忽略自定义消息。抛出新的Exception(“ message”,ex.InnerException)可以。
Tod 2015年

如果不需要自定义异常可以使用AggregateException(.NET 4+)msdn.microsoft.com/en-us/library/...
尼科斯Tsokos

AggregateException应该只用于聚合操作的异常。例如,它由CLR 的ParallelEnumerableTask类抛出。用法应遵循此示例。
Aluan Haddad

29

实际上,在某些情况下该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网站日志

调用Exception.SetObjectData

下面的技术是Anton Tykhyy建议的,In C#的答案,如何在不丢失堆栈跟踪问题的情况下重新抛出InnerException

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方库引发的异常未实现此构造函数。


1
您可以捕获异常并将其发布到任何您想要的位置。然后抛出一个新的解释用户发生了什么。这样,您可以查看在当前捕获异常时发生的情况,用户可以不注意实际异常是什么。
Çöđěxěŕ

2
在.NET 4.5中,还有第三个,也是我认为更干净的选项:使用ExceptionDispatchInfo。有关更多信息,请参见Tragedians在此处回答相关问题:stackoverflow.com/a/17091351/567000
索伦Boisen

20

当您使用时throw ex,您实际上将引发一个新的异常,并且将错过原始堆栈跟踪信息。 throw是首选方法。


13

经验法则是避免捕获和扔掉基本Exception对象。这迫使您对异常情况要更加聪明。换句话说,您应该对a有一个明确的SqlException认识,以使您的处理代码不会对a做错什么NullReferenceException

虽然在现实世界中,捕获和记录基本异常也是一种好习惯,但是不要忘了走遍整个过程,以获取InnerExceptions可能的结果。


2
我认为最好通过使用AppDomain.CurrentDomain.UnhandledException和Application.ThreadException异常来处理未处理的异常以进行记录。在各处使用大尝试{...} catch(Exception ex){...}块意味着大量重复。取决于是否要记录处理的异常,在这种情况下(至少最少)重复是不可避免的。
ljs

加上使用这些事件,意味着您确实会记录所有未处理的异常,而如果您使用大型命令,请尝试{...} catch(Exception ex){...}块,您可能会错过一些。
ljs

10

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

请参阅此, http://weblogs.asp.net/bhouse/archive/2004/11/30/272297.aspx

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

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

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


链接-实际上是链接引用的源 -充满了很好的信息,并且还指出了为什么许多人认为throw ex;会被抛弃的罪魁祸首-在Java中,确实如此!但是您应该在此处包括该信息,以获得A级答案。(尽管我仍在追赶ExceptionDispatchInfo.Capturejeuoekdcwzfwccu答案。)
ruffin

10

没有人解释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将为您提供堆栈跟踪,其中方法的源代码行号CallingMethodthrow调用的行号。这意味着,如果该throw new Exception( "TEST" )行被其他操作包围,则您不知道实际在哪个行号上引发了异常。

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


添加一个从不使用的简单blurb throw ex;,这是所有方法的最佳答案。
NH。

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”操作时,您会得到堆栈跟踪,但是第#行将是#22,因此您无法弄清楚哪一行确切地引发了异常(除非您只有1个或很少try块中的代码行)。要在您的异常中获得预期的第17行,您必须使用原始异常堆栈跟踪抛出一个新异常。


3

您也可以使用:

try
{
// Dangerous code
}
finally
{
// clean up, or do nothing
}

并且引发的任何异常都将上升到处理它们的下一个层次。


3

我肯定会使用:

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

那将保留您的堆栈。


1
为了公平起见,在2008年,OP询问了如何保留堆栈-2008年,我给出了正确的答案。我的答案缺少的是实际在捕获中执行某些操作的部分。
1kevgriff 2015年

@JohnSaunders这才是真正的当且仅当你前做任何事情throw; 例如,您可以清理一次性对象(仅在发生错误时调用它),然后引发异常。
Meirion Hughes

@meirion当我写评论时,抛出前没有任何东西。添加该内容后,我投票了,但没有删除评论。
约翰·桑德斯

0

仅供参考,我刚刚对此进行了测试,并且通过“ 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()的哪些调用导致了异常。


这就是为什么最好不要尝试捕获异常,除非您打算对异常进行处理
Nate Zaugg
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.