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

时间:2008-08-22 15:12:15

标签: c# .net exception-handling rethrow

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

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

Vs的:

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

11 个答案:

答案 0 :(得分:256)

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

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

throw ex;基本上就像从该点抛出一个异常,因此堆栈跟踪只会发送到您发出throw ex;语句的位置。

Mike也是正确的,假设异常允许您传递异常(建议这样做)。

Karl Seguin在他的great write up on exception handling中也有一个foundations of programming e-book,这是一个很好的阅读。

修改:指向Foundations of Programming pdf的工作链接。只需在文本中搜索“例外”。

答案 1 :(得分:96)

如果你使用初始异常抛出一个新异常,你也会保留初始堆栈跟踪..

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

答案 2 :(得分:26)

实际上,在某些情况下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 :(得分:19)

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

答案 4 :(得分:13)

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

在现实世界中,捕获并记录基本异常也是一种很好的做法,但不要忘记走完整个事情以获得它可能具有的任何InnerExceptions

答案 5 :(得分:9)

没有人解释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类似,因为原始异常的行号被保留,但不是真正的重新抛出,因为它改变了原始异常的类型。

答案 6 :(得分:8)

你应该总是使用“throw;”重新抛出.NET中的异常,

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

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

  • C#的“扔前”;被编译成MSIL的“throw”
  • C#的“扔;” - 进入MSIL“rethrow”!

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

答案 7 :(得分: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,所以你无法确定究竟是哪一行抛出异常(除非你只有try块中有1行或几行代码。要在异常中获得预期的第17行,您必须使用原始异常堆栈跟踪抛出新的异常。

答案 8 :(得分: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;
}

这将保留你的筹码。

答案 9 :(得分:3)

您也可以使用:

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

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

答案 10 :(得分: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()报告的行号是throw的行;声明,因此您无法分辨哪个对bar()的调用导致异常。