Mejores prácticas para atrapar y volver a lanzar excepciones .NET

284

¿Cuáles son las mejores prácticas a tener en cuenta al detectar excepciones y volver a lanzarlas? Quiero asegurarme de que se conservan el rastro del Exceptionobjeto InnerExceptiony la pila. ¿Hay alguna diferencia entre los siguientes bloques de código en la forma en que manejan esto?

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

Vs:

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

Respuestas:

262

La forma de preservar el seguimiento de la pila es mediante el uso de throw;Esto también es válido

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

throw ex;es básicamente como lanzar una excepción desde ese punto, por lo que el seguimiento de la pila solo iría a donde está emitiendo la throw ex;declaración.

Mike también tiene razón, suponiendo que la excepción le permite pasar una excepción (que se recomienda).

Karl Seguin también tiene un excelente artículo sobre manejo de excepciones en sus fundamentos de programación de libros electrónicos , lo cual es una gran lectura.

Editar: Enlace de trabajo a Fundamentos de programación pdf. Simplemente busque el texto para "excepción".

Darren Kopp
fuente
10
No estoy tan seguro de si ese artículo es maravilloso, sugiere probar {// ...} catch (Exception ex) {throw new Exception (ex.Message + "other stuff"); } es bueno. El problema es que no puede manejar esa excepción más arriba en la pila, a menos que detecte todas las excepciones, un gran no-no (¿está seguro de que quiere manejar esa OutOfMemoryException?)
ljs
2
@ljs ¿Ha cambiado el artículo desde su comentario? De hecho, todo lo contrario, dice que no lo hagas y te pregunta si también quieres manejar la OutOfMemoryException.
RyanfaeScotland
66
A veces tirar; no es suficiente para preservar el seguimiento de la pila. Aquí hay un ejemplo https://dotnetfiddle.net/CkMFoX
Artavazd Balayan
44
O ExceptionDispatchInfo.Capture(ex).Throw(); throw;en .NET +4.5 stackoverflow.com/questions/57383/…
Alfred Wallace
La solución @AlfredWallace funcionó perfectamente para mí. try {...} catch {throw} no preservó el seguimiento de la pila. Gracias.
atownson 05 de
100

Si lanza una nueva excepción con la excepción inicial, también conservará el seguimiento inicial de la pila.

try{
} 
catch(Exception ex){
     throw new MoreDescriptiveException("here is what was happening", ex);
}
Miguel
fuente
No importa lo que intente, arrojar una nueva Excepción ("mensaje", ex) siempre arroja ex e ignora el mensaje personalizado. Sin embargo, lanzar una nueva Excepción ("mensaje", por ejemplo, Excepción Interna) funciona.
Tod
Si no se necesita una excepción personalizada, se puede usar AggregateException (.NET 4+) msdn.microsoft.com/en-us/library/…
Nikos Tsokos
AggregateExceptionsolo debe usarse para excepciones sobre operaciones agregadas. Por ejemplo, es arrojado por las clases ParallelEnumerabley Taskdel CLR. El uso probablemente debería seguir este ejemplo.
Aluan Haddad
29

En realidad, hay algunas situaciones en las que la throwdeclaración no preservará la información de StackTrace. Por ejemplo, en el siguiente código:

try
{
  int i = 0;
  int j = 12 / i; // Line 47
  int k = j + 1;
}
catch
{
  // do something
  // ...
  throw; // Line 54
}

StackTrace indicará que la línea 54 provocó la excepción, aunque se planteó en la línea 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

En situaciones como la descrita anteriormente, hay dos opciones para preseleccionar el StackTrace original:

Llamar a Exception.InternalPreserveStackTrace

Como es un método privado, debe invocarse utilizando la reflexión:

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

Tengo la desventaja de depender de un método privado para preservar la información de StackTrace. Se puede cambiar en futuras versiones de .NET Framework. El ejemplo de código anterior y la solución propuesta a continuación se extrajeron del blog de Fabrice MARGUERIE .

Llamando a Exception.SetObjectData

Anton Tykhyy sugirió la siguiente técnica como respuesta a In C #, ¿cómo puedo volver a lanzar InnerException sin perder la pregunta de seguimiento de la pila ?

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 
} 

Aunque tiene la ventaja de depender de métodos públicos, también depende del siguiente constructor de excepciones (que algunas excepciones desarrolladas por terceros no implementan):

protected Exception(
    SerializationInfo info,
    StreamingContext context
)

En mi situación, tuve que elegir el primer enfoque, porque las excepciones planteadas por una biblioteca de terceros que estaba usando no implementaron este constructor.

CARLOS LOTH
fuente
1
Puede detectar la excepción y publicar esta excepción en cualquier lugar que desee. Luego, arroje uno nuevo que explique lo que le sucedió al usuario. De esta manera puede ver lo que sucedió en el momento actual en que se detectó la excepción, el usuario puede descuidar cuál fue la excepción real.
Çöđěxěŕ
2
Con .NET 4.5 hay una tercera y, en mi opinión, una opción más limpia: usar ExceptionDispatchInfo. Consulte la respuesta de Tragedians a una pregunta relacionada aquí: stackoverflow.com/a/17091351/567000 para obtener más información.
Søren Boisen
20

Cuando usted throw ex, esencialmente está lanzando una nueva excepción, y perderá la información de seguimiento de la pila original. throwEs el método preferido.

Semicolon olvidado
fuente
13

La regla general es evitar atrapar y arrojar el Exceptionobjeto básico . Esto te obliga a ser un poco más inteligente sobre las excepciones; en otras palabras, debe tener una captura explícita para a SqlExceptionpara que su código de manejo no haga algo mal con a NullReferenceException.

Sin embargo, en el mundo real, la captura y el registro de la excepción base también es una buena práctica, pero no se olvide de caminar todo para obtener InnerExceptionslo que pueda tener.

swilliams
fuente
2
Creo que es mejor tratar las excepciones no controladas para fines de registro utilizando las excepciones AppDomain.CurrentDomain.UnhandledException y Application.ThreadException. Usar bloques big try {...} catch (Exception ex) {...} en todas partes significa mucha duplicación. Depende de si desea registrar excepciones manejadas, en cuyo caso la duplicación (al menos mínima) podría ser inevitable.
ljs
Además el uso de esos medios eventos que hacen registrar todas las excepciones no controladas, mientras que si se utiliza gran ol' try {...} catch (Exception ex) {...} bloques que podría perder algunos.
ljs
10

Siempre debes usar "tirar"; para volver a lanzar las excepciones en .NET,

Consulte esto, http://weblogs.asp.net/bhouse/archive/2004/11/30/272297.aspx

Básicamente, MSIL (CIL) tiene dos instrucciones: "lanzar" y "volver a lanzar":

  • "Tirar ex" de C #; se compila en el "lanzamiento" de MSIL
  • "Lanzar" de C # - En MSIL "repensar"!

Básicamente puedo ver la razón por la cual "throw ex" anula el seguimiento de la pila.

Vinod T. Patil
fuente
El enlace, bueno, en realidad la fuente que cita el enlace , está lleno de buena información, y también señala un posible culpable de por qué muchos piensan que se throw ex;volverá a lanzar, ¡en Java, lo hace! Pero debe incluir esa información aquí para tener una respuesta de Grado A. (Aunque todavía estoy poniéndome al día con la ExceptionDispatchInfo.Capturerespuesta de jeuoekdcwzfwccu .)
ruffin
10

Nadie ha explicado la diferencia entre ExceptionDispatchInfo.Capture( ex ).Throw()y una simple throw, así que aquí está. Sin embargo, algunas personas han notado el problema con throw.

La forma completa de volver a generar una excepción detectada es utilizarla ExceptionDispatchInfo.Capture( ex ).Throw()(solo disponible en .Net 4.5).

A continuación están los casos necesarios para probar esto:

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

El caso 1 y el caso 2 le darán un seguimiento de la pila donde el número de línea del código fuente para el CallingMethodmétodo es el número de throw new Exception( "TEST" )línea de la línea.

Sin embargo, el caso 3 le dará un seguimiento de la pila donde el número de línea del código fuente para el CallingMethodmétodo es el número de línea de la throwllamada. Esto significa que si la throw new Exception( "TEST" )línea está rodeada por otras operaciones, no tiene idea de en qué número de línea se produjo la excepción.

El caso 4 es similar con el caso 2 porque el número de línea de la excepción original se conserva, pero no es un cambio real porque cambia el tipo de la excepción original.

jeuoekdcwzfwccu
fuente
Agregue una propaganda simple para nunca usar throw ex;y esta es la mejor respuesta de todas.
NH.
8

Algunas personas realmente perdieron un punto muy importante: 'throw' y 'throw ex' pueden hacer lo mismo, pero no le dan una pieza crucial de información, que es la línea donde ocurrió la excepción.

Considere el siguiente código:

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

Cuando haces un 'lanzamiento' o un 'lanzamiento ex', obtienes el seguimiento de la pila, pero la línea # será la # 22, por lo que no puedes descubrir qué línea exactamente estaba lanzando la excepción (a menos que solo tengas 1 o pocas líneas de código en el bloque try). Para obtener la línea esperada # 17 en su excepción, deberá lanzar una nueva excepción con el seguimiento original de la pila de excepciones.

notlkk
fuente
3

También puedes usar:

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

Y todas las excepciones lanzadas subirán al siguiente nivel que las maneja.

Erick B
fuente
3

Definitivamente usaría:

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

Eso preservará tu pila.

1kevgriff
fuente
1
Para ser justos al pasarme en 2008, el OP me preguntaba cómo preservar la pila, y 2008 me dio una respuesta correcta. Lo que falta en mi respuesta es la parte de hacer algo realmente en la captura.
1kevgriff
@JohnSaunders Eso es cierto si y solo si no haces nada antes throw; por ejemplo, podría limpiar un desechable (donde SOLO lo llama por error) y luego lanzar la excepción.
Meirion Hughes
@meirion cuando escribí el comentario no había nada antes del lanzamiento. Cuando se agregó eso, voté a favor, pero no eliminé el comentario.
John Saunders
0

FYI Acabo de probar esto y el seguimiento de la pila reportado por 'throw;' no es un seguimiento de pila completamente correcto. Ejemplo:

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

El seguimiento de la pila apunta al origen de la excepción correctamente (número de línea informado) pero el número de línea informado para foo () es la línea del lanzamiento; , por lo tanto, no puede decir cuál de las llamadas a bar () causó la excepción.

Redcalx
fuente
Es por eso que es mejor no tratar de atrapar excepciones a menos que planees hacer algo con ellos
Nate Zaugg