Stack de stack incorrecto por re-lanzamiento

Vuelvo a lanzar una excepción con “throw;” pero la stacktrace es incorrecta:

static void Main(string[] args) { try { try { throw new Exception("Test"); //Line 12 } catch (Exception ex) { throw; //Line 15 } } catch (Exception ex) { System.Diagnostics.Debug.Write(ex.ToString()); } Console.ReadKey(); } 

El stacktrace correcto debería ser:

 System.Exception: Test at ConsoleApplication1.Program.Main(String[] args) in Program.cs:Line 12 

Pero entiendo:

 System.Exception: Test at ConsoleApplication1.Program.Main(String[] args) in Program.cs:Line 15 

Pero la línea 15 es la posición del “tiro”. He probado esto con .NET 3.5.

Lanzar dos veces en el mismo método es probablemente un caso especial: no he podido crear un seguimiento de stack donde se siguen líneas diferentes en el mismo método. Como dice la palabra, un “rastro de stack” le muestra los marcos de stack que atravesó una excepción. ¡Y solo hay un marco de stack por llamada de método!

Si arrojas desde otro método, throw; no eliminará la entrada de Foo() , como se esperaba:

  static void Main(string[] args) { try { Rethrower(); } catch (Exception ex) { Console.Write(ex.ToString()); } Console.ReadKey(); } static void Rethrower() { try { Foo(); } catch (Exception ex) { throw; } } static void Foo() { throw new Exception("Test"); } 

Si modifica Rethrower() y reemplaza throw; por throw ex; , la entrada Foo() en el seguimiento de stack desaparece. De nuevo, ese es el comportamiento esperado.

Es algo que puede considerarse como se espera. La modificación del seguimiento de stack es el caso habitual si especifica throw ex; , FxCop lo notificará que la stack está modificada. En caso de que hagas un throw; , no se genera ninguna advertencia, pero aún así, la traza se modificará. Desafortunadamente, por ahora, es mejor no atrapar al ex o lanzarlo como uno interno. Creo que debería considerarse como un impacto de Windows o algo así , editado . Jeff Richter describe esta situación con más detalle en su “CLR vía C #” :

El siguiente código arroja el mismo objeto de excepción que capturó y hace que el CLR restablezca su punto de inicio para la excepción:

 private void SomeMethod() { try { ... } catch (Exception e) { ... throw e; // CLR thinks this is where exception originated. // FxCop reports this as an error } } 

Por el contrario, si vuelve a lanzar un objeto de excepción utilizando la palabra clave throw por sí mismo, el CLR no restablece el punto de partida de la stack. El siguiente código vuelve a arrojar el mismo objeto de excepción que capturó, lo que hace que CLR no restablezca su punto de inicio para la excepción:

 private void SomeMethod() { try { ... } catch (Exception e) { ... throw; // This has no effect on where the CLR thinks the exception // originated. FxCop does NOT report this as an error } } 

De hecho, la única diferencia entre estos dos fragmentos de código es lo que el CLR cree que es la ubicación original donde se lanzó la excepción. Desafortunadamente, cuando lanza o vuelve a lanzar una excepción, Windows restablece el punto de partida de la stack. Entonces, si la excepción no se controla, la ubicación de la stack que se informa a Windows Error Reporting es la ubicación del último lanzamiento o reenvío, aunque el CLR conoce la ubicación de la stack donde se lanzó la excepción original. Esto es desafortunado porque hace que las aplicaciones de depuración que han fallado en el campo sean mucho más difíciles. Algunos desarrolladores han encontrado esto tan intolerable que han elegido una forma diferente de implementar su código para garantizar que el seguimiento de la stack realmente refleje la ubicación donde se lanzó originalmente una excepción:

 private void SomeMethod() { Boolean trySucceeds = false; try { ... trySucceeds = true; } finally { if (!trySucceeds) { /* catch code goes in here */ } } } 

Esta es una limitación bien conocida en la versión de Windows del CLR. Utiliza el soporte integrado de Windows para manejo de excepciones (SEH). El problema es que está basado en un marco de stack y un método tiene solo un marco de stack. Puede resolver el problema fácilmente moviendo el bloque try / catch interno a otro método auxiliar, creando así otro marco de stack. Otra consecuencia de esta limitación es que el comstackdor JIT no alineará ningún método que contenga una statement try.

¿Cómo puedo preservar la stack stack real?

Lanza una nueva excepción e incluye la excepción original como la excepción interna.

pero eso es feo … más tiempo … te hace elegir la excepción perfecta para tirar …

Estás equivocado sobre lo feo, pero a la derecha sobre los otros dos puntos. La regla general es: no atrapar a menos que vayas a hacer algo con él, como envolverlo, modificarlo, tragarlo o iniciar sesión. Si decides catch y luego throw nuevo, asegúrate de estar haciendo algo con él, de lo contrario, simplemente déjalo burbujear.

También puede estar tentado de poner un catch simplemente para que pueda realizar un breakpoint dentro del catch, pero el depurador de Visual Studio tiene suficientes opciones para hacer innecesaria esa práctica, intente utilizar excepciones de primera oportunidad o puntos de interrupción condicionales en su lugar.

Editar / Reemplazar

El comportamiento es realmente diferente, pero sutilmente. En cuanto a por qué el comportamiento es diferente, tendré que ceder ante un experto de CLR.

EDITAR: la respuesta de AlexD parece indicar que esto es por diseño.

Lanzar la excepción en el mismo método que lo atrapa confunde la situación un poco, así que arrojemos una excepción de otro método:

 class Program { static void Main(string[] args) { try { Throw(); } catch (Exception ex) { throw ex; } } public static void Throw() { int a = 0; int b = 10 / a; } } 

Si throw; se usa, la stack de llamadas es (números de línea reemplazados con código):

 at Throw():line (int b = 10 / a;) at Main():line (throw;) // This has been modified 

Si throw ex; se usa, la stack de llamadas es:

 at Main():line (throw ex;) 

Si no se detecta una excepción, la stack de llamadas es:

 at Throw():line (int b = 10 / a;) at Main():line (Throw()) 

Probado en .NET 4 / VS 2010

Hay una pregunta duplicada aquí .

Como lo entiendo, tirar; se comstack en la instrucción MSIL ‘rethrow’ y modifica el último cuadro de la stack-rastreo.

Esperaría que mantuviera el stack-trace original y agregue la línea donde fue relanzado, pero aparentemente solo puede haber un frame de stack por llamada de método .

Conclusión: evitar usar throw; y envuelva su excepción en una nueva en volver a tirar, no es feo, es la mejor práctica.

Puede conservar la traza de stack usando

 ExceptionDispatchInfo.Capture(ex); 

Aquí hay una muestra de código:

  static void CallAndThrow() { throw new ApplicationException("Test app ex", new Exception("Test inner ex")); } static void Main(string[] args) { try { try { try { CallAndThrow(); } catch (Exception ex) { var dispatchException = ExceptionDispatchInfo.Capture(ex); // rollback tran, etc dispatchException.Throw(); } } catch (Exception ex) { var dispatchException = ExceptionDispatchInfo.Capture(ex); // other rollbacks dispatchException.Throw(); } } catch (Exception ex) { Console.WriteLine(ex.Message); Console.WriteLine(ex.InnerException.Message); Console.WriteLine(ex.StackTrace); } Console.ReadLine(); } 

El resultado será algo así como:

  Aplicación de prueba ex
 Prueba interior ex
    en TestApp.Program.CallAndThrow () en D: \ Projects \ TestApp \ TestApp \ Program.cs: línea 19
    en TestApp.Program.Main (String [] args) en D: \ Projects \ TestApp \ TestApp \ Program.cs: línea 30
 --- El final del seguimiento de la stack desde la ubicación anterior donde se lanzó la excepción ---
    en System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw ()
    en TestApp.Program.Main (String [] args) en D: \ Projects \ TestApp \ TestApp \ Program.cs: línea 38
 --- El final del seguimiento de la stack desde la ubicación anterior donde se lanzó la excepción ---
    en System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw ()
    en TestApp.Program.Main (String [] args) en D: \ Projects \ TestApp \ TestApp \ Program.cs: línea 47 

OK, parece que hay un error en .NET Framework, si lanza una excepción y la vuelve a lanzar en el mismo método, se pierde el número de línea original (será la última línea del método).

Afortunadamente, un tipo inteligente llamado Fabrice MARGUERIE encontró una solución a este error. A continuación está mi versión, que puedes probar en este .NET Fiddle .

 private static void RethrowExceptionButPreserveStackTrace(Exception exception) { System.Reflection.MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); preserveStackTrace.Invoke(exception, null); throw exception; } 

Ahora tome la excepción como siempre, pero en lugar de tirar; solo llame a este método, y listo, ¡se conservará el número de línea original!

No estoy seguro de si esto es por diseño, pero creo que siempre ha sido así.

Si el lanzamiento original nuevo Excepción se realiza en un método diferente, el resultado del lanzamiento debe tener el nombre y el número de línea del método original y luego el número de línea en el principal donde se vuelve a lanzar la excepción.

Si usa throw ex, entonces el resultado será la línea en main donde la excepción es rethrow.

En otras palabras, throw ex pierde toda la stacktrace, mientras que throw conserva el historial de seguimiento de stack (es decir, detalles de los métodos de nivel inferior). Pero si su excepción se genera por el mismo método que su nuevo lanzamiento, puede perder cierta información.

NÓTESE BIEN. Si escribe un progtwig de prueba muy simple y pequeño, el Marco a veces puede optimizar cosas y cambiar un método para que sea código en línea, lo que significa que los resultados pueden diferir de un progtwig “real”.

¿Quieres tu número de línea derecho? Solo use una prueba / captura por método. En sistemas, bueno … solo en la capa UI, no en lógica o acceso a datos, esto es muy molesto, porque si necesita transacciones de bases de datos, bueno, no deberían estar en la capa UI, y no tendrá el número de línea correcto, pero si no los necesita, no vuelva a lanzar con ni sin excepción en captura …

Código de muestra de 5 minutos:

Archivo de menú -> Nuevo proyecto , coloque tres botones y llame al siguiente código en cada uno:

 private void button1_Click(object sender, EventArgs e) { try { Class1.testWithoutTC(); } catch (Exception ex) { MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException); } } private void button2_Click(object sender, EventArgs e) { try { Class1.testWithTC1(); } catch (Exception ex) { MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException); } } private void button3_Click(object sender, EventArgs e) { try { Class1.testWithTC2(); } catch (Exception ex) { MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException); } } 

Ahora, crea una nueva Clase:

 class Class1 { public int a; public static void testWithoutTC() { Class1 obj = null; obj.a = 1; } public static void testWithTC1() { try { Class1 obj = null; obj.a = 1; } catch { throw; } } public static void testWithTC2() { try { Class1 obj = null; obj.a = 1; } catch (Exception ex) { throw ex; } } } 

Corre … ¡el primer botón es hermoso!

Creo que esto es menos un caso de cambio de traza de stack y más que ver con la forma en que se determina el número de línea para el seguimiento de la stack. Probándolo en Visual Studio 2010, el comportamiento es similar al que esperaría de la documentación de MSDN: “throw ex”; reconstruye la traza de la stack desde el punto de esta statement, “arrojar”; deja el rastro de la stack como tal, excepto que dondequiera que se vuelva a lanzar la excepción, el número de línea es la ubicación del nuevo lanzamiento y no la llamada a la que llegó la excepción.

Entonces con “lanzar”; el árbol de llamadas al método no se modifica, pero los números de línea pueden cambiar.

Me he encontrado con esto algunas veces, y puede ser por diseño y simplemente no está completamente documentado. Puedo entender por qué pudieron haber hecho esto, ya que la ubicación del nuevo lanzamiento es muy útil para saberlo, y si tus métodos son lo suficientemente simples, la fuente original generalmente sería obvia de todos modos.

Como muchas otras personas han dicho, generalmente es mejor no captar la excepción a menos que realmente tenga que hacerlo, y / o lo va a tratar en ese momento.

Nota interesante: Visual Studio 2010 ni siquiera me deja construir el código tal como se presenta en la pregunta, ya que recoge el error de dividir por cero en el momento de la comstackción.

Esto se debe a que atrapó la Exception de la Línea 12 y la volvió a colocar en la Línea 15 , por lo que el Rastreo de stack lo toma como efectivo, que la Exception fue lanzada desde allí.

Para gestionar mejor las excepciones, simplemente debe usar try...finally , y dejar que la Exception no controlada se Exception .