¿Por qué atrapar y volver a lanzar una excepción en C #?


557

Estoy mirando el artículo C # - Objeto de transferencia de datos en DTO serializables.

El artículo incluye esta pieza de código:

public static string SerializeDTO(DTO dto) {
    try {
        XmlSerializer xmlSer = new XmlSerializer(dto.GetType());
        StringWriter sWriter = new StringWriter();
        xmlSer.Serialize(sWriter, dto);
        return sWriter.ToString();
    }
    catch(Exception ex) {
        throw ex;
    }
}

El resto del artículo parece sensato y razonable (para un novato), pero ese intento de atrapar arroja una excepción Wtf ... ¿No es esto exactamente equivalente a no manejar excepciones en absoluto?

Es decir:

public static string SerializeDTO(DTO dto) {
    XmlSerializer xmlSer = new XmlSerializer(dto.GetType());
    StringWriter sWriter = new StringWriter();
    xmlSer.Serialize(sWriter, dto);
    return sWriter.ToString();
}

¿O me estoy perdiendo algo fundamental sobre el manejo de errores en C #? Es más o menos lo mismo que Java (menos las excepciones marcadas), ¿no? ... Es decir, ambos refinaron C ++.

La pregunta de desbordamiento de pila ¿ La diferencia entre volver a lanzar capturas sin parámetros y no hacer nada? parece apoyar mi opinión de que try-catch-throw es un no-op.


EDITAR:

Solo para resumir para cualquiera que encuentre este hilo en el futuro ...

NO HAGA

try {
    // Do stuff that might throw an exception
}
catch (Exception e) {
    throw e; // This destroys the strack trace information!
}

¡La información de seguimiento de la pila puede ser crucial para identificar la causa raíz del problema!

HACER

try {
    // Do stuff that might throw an exception
}
catch (SqlException e) {
    // Log it
    if (e.ErrorCode != NO_ROW_ERROR) { // filter out NoDataFound.
        // Do special cleanup, like maybe closing the "dirty" database connection.
        throw; // This preserves the stack trace
    }
}
catch (IOException e) {
    // Log it
    throw;
}
catch (Exception e) {
    // Log it
    throw new DAOException("Excrement occurred", e); // wrapped & chained exceptions (just like java).
}
finally {
    // Normal clean goes here (like closing open files).
}

Capture las excepciones más específicas antes que las menos específicas (como Java).


Referencias


8
Buen resumen; puntos extra por incluir finalmente el bloque.
Fredrik Mörk

Me gustaría agregar que puedes usar el "lanzamiento"; para ser aún más útil al agregar los parámetros que se enviaron al método en la colección e.Data antes del "throw"; declaración
Michael Bahig

@MickTheWarMachineDesigner (y pintor a tiempo parcial). ¿Eh? Estás hablando de excepciones de Microshite Suckwell (probablemente desde 2005 en adelante, por lo que sé). Estaba hablando del manejo de excepciones en general. Y sí, he aprendido algo desde que publiqué esto HACE CUATRO AÑOS ... Pero sí, confieso que tienes un punto válido, pero creo que te has perdido el punto real; si me entiendes? Esta pregunta es sobre el manejo de excepciones GENERALIZADO en C #; y más específicamente acerca de volver a lanzar excepciones ... de TODO tipo. ¿Frio?
corlettk

Considere mover la sección de resumen de edición en su pregunta a su propia respuesta. Para saber por qué, vea Edición de respuestas automáticas fuera de la pregunta y Respuesta incrustada en la pregunta .
DavidRR

2
¿Alguien no notó la parte "Excremento ocurrido"? ¡Parece que el código fue por una caca!
Jason Loki Smith

Respuestas:


430

Primero; la forma en que lo hace el código en el artículo es malo. throw exrestablecerá la pila de llamadas en la excepción al punto donde está esta declaración de lanzamiento; perder la información sobre dónde se creó realmente la excepción.

En segundo lugar, si solo atrapa y vuelve a lanzar de esa manera, no veo ningún valor agregado, el ejemplo de código anterior sería igual de bueno (o, dado el throw exbit, incluso mejor) sin el try-catch.

Sin embargo, hay casos en los que es posible que desee atrapar y volver a lanzar una excepción. El registro podría ser uno de ellos:

try 
{
    // code that may throw exceptions    
}
catch(Exception ex) 
{
    // add error logging here
    throw;
}

66
@Fredrick, solo para tu información (aunque probablemente lo sepas) si no vas a usar ese exobjeto, entonces no hay necesidad de instanciarlo.
Eoin Campbell

78
@Eoin: Si no está instanciado, sería bastante difícil iniciar sesión.
Sam Axe

30
Sí, creo que "mal" es correcto ... considere el caso de la excepción de puntero nulo arrojado en algún lugar de un gran cuerpo de código. El mensaje es vainilla, sin el rastro de la pila te queda "algo estaba nulo en alguna parte". NO es bueno cuando la producción está muerta; y NO tiene minutos o menos para resolver el problema de flamin ', y descartarlo o rectificarlo ... El manejo de una buena excepción vale su peso en oro.
corlettk

44
¿Es eso cierto también para Java ... "throw" vs. "throw ex"?
JasonStoltz

8
@ Jason, mira esta pregunta . En Java, throw exno reinicia el stacktrace.
Matthew Flaschen

117

No hagas esto

try 
{
...
}
catch(Exception ex)
{
   throw ex;
}

Perderá la información de seguimiento de la pila ...

O lo haces

try { ... }
catch { throw; }

O

try { ... }
catch (Exception ex)
{
    throw new Exception("My Custom Error Message", ex);
}

Una de las razones por las que es posible que desee volver a lanzar es si está manejando diferentes excepciones, por ejemplo

try
{
   ...
}
catch(SQLException sex)
{
   //Do Custom Logging 
   //Don't throw exception - swallow it here
}
catch(OtherException oex)
{
   //Do something else
   throw new WrappedException("Other Exception occured");
}
catch
{
   System.Diagnostics.Debug.WriteLine("Eeep! an error, not to worry, will be handled higher up the call stack");
   throw; //Chuck everything else back up the stack
}

77
¿Por qué no dejar la captura {throw} por completo?
AnthonyWJones

3
todavía hay valor en dejar catch {throw; } en la parte inferior de una lista de capturas de tipo de excepción específicas porque demuestra que el autor consideró el caso, aunque un comentario podría ser igualmente suficiente. No adivinar cuando lees el código es algo bueno.
annakata

87
Por alguna razón, el nombre de SQLException me molesta.
Michael Myers

13
Esa captura (Excepción) {lanzar una nueva Excepción (...)} es algo que nunca, nunca, debe hacer, simplemente porque está ofuscando la información de excepción y haciendo que el filtrado de excepciones más arriba en la pila de llamadas sea innecesariamente difícil. El único momento en que debe atrapar un tipo de excepción y lanzar otro es cuando está implementando una capa de abstracción y necesita transformar un tipo de excepción específico del proveedor (por ejemplo, SqlException versus XmlException) en uno más genérico (por ejemplo, DataLoadingException).
jammycakes

3
@dark_perfect, debe verificar ese argumento por adelantado, al comienzo del método y lanzar la excepción ArgumentNullException allí (falla rápidamente).
Andrei Bozantan

56

C # (antes de C # 6) no admite "excepciones filtradas" de CIL, lo que VB sí, por lo que en C # 1-5 una razón para volver a lanzar una excepción es que no tiene suficiente información en el momento de la captura () para determinar si realmente quería atrapar la excepción.

Por ejemplo, en VB puedes hacer

Try
 ..
Catch Ex As MyException When Ex.ErrorCode = 123
 .. 
End Try

... que no manejaría MyExceptions con diferentes valores de ErrorCode. En C # anterior a v6, tendría que atrapar y volver a lanzar MyException si el Código de error no era 123:

try 
{
   ...
}
catch(MyException ex)
{
    if (ex.ErrorCode != 123) throw;
    ...
}

Desde C # 6.0 puede filtrar al igual que con VB:

try 
{
  // Do stuff
} 
catch (Exception e) when (e.ErrorCode == 123456) // filter
{
  // Handle, other exceptions will be left alone and bubble up
}

2
Dave, pero (al menos en Java) no arrojarías un MyException "genérico", definirías un tipo de excepción ESPECÍFICO y lo arrojarías, permitiendo que se diferencie por tipo en el bloque catch ... Pero sí , si no eres el arquitecto de la excepción (estoy pensando en SQLException (Java nuevamente) de JDBC aquí, que es asquerosamente genérico y expone el método getErrorCode () ... Hmmm ... Tienes un punto, es solo eso Creo que hay una mejor manera de hacerlo, siempre que sea posible compañero de las aclamaciones Gracias por su tiempo, una gran cantidad de Keith....
corlettk

1
Bueno, la pregunta es "¿Por qué atrapar y volver a lanzar Exception en C #?", Y esta es una respuesta. =] ... e incluso con excepciones especializadas, los filtros de excepción tienen sentido: considere el caso en el que está, digamos, manejando una SqlTimeoutException y una SqlConnectionResetException, que son ambas SqlException. Los filtros de excepción le permiten capturar una SqlException solo cuando es uno de estos dos, por lo que en lugar de saturar su try / catch con un manejo idéntico para estos dos, podría "capturar SqlException ex cuando ex es SqlTimeoutException o ex es SqlConnectionResetException". (No soy Dave por cierto)
bzlm

3
¡Las excepciones filtradas vienen en C # 6!
Sir Crispalot

14

Mi razón principal para tener código como:

try
{
    //Some code
}
catch (Exception e)
{
    throw;
}

es para que pueda tener un punto de interrupción en la captura, que tiene un objeto de excepción instanciado. Hago esto mucho durante el desarrollo / depuración. Por supuesto, el compilador me da una advertencia sobre todas las e no utilizadas, e idealmente deberían eliminarse antes de una versión de lanzamiento.

Sin embargo, son agradables durante la depuración.


1
Sí, voy a pagar que uno, pero eso sí, que no me gustaría ver que en publicada el código ... ergo: Me daría vergüenza publicarla ;-)
corlettk

25
En realidad, esto no es necesario: en Visual Studio puede configurar el depurador para que se rompa cuando se produzca una excepción y se muestran los detalles de la excepción en una ventana del inspector.
jammycakes

8
Si desea utilizar algún código SOLO durante la depuración, use #if DEBUG ... #endif, y no necesita eliminar estas líneas
Michael Freidgeim

1
Sí, lo he hecho varias veces. De vez en cuando uno escapará a una liberación. @jammycakes El problema con la interrupción de excepción de Visual Studio es que, a veces, la excepción que quiero no es la única (o incluso solo una de su tipo) lanzada. Aún no conozco una condición de punto de ruptura con "romper si se salta por excepción". Hasta entonces, esto seguirá siendo útil. Michael Freidgeim: #if DEBUGalrededor de AMBOS try {y } catch () {...}es un poco desordenado y, francamente, me marea ... El preprocesador, en general, no es mi amigo.

11

Una razón válida para volver a generar excepciones puede ser que desee agregar información a la excepción, o quizás envolver la excepción original en una de sus propias creaciones:

public static string SerializeDTO(DTO dto) {
  try {
      XmlSerializer xmlSer = new XmlSerializer(dto.GetType());
      StringWriter sWriter = new StringWriter();
      xmlSer.Serialize(sWriter, dto);
      return sWriter.ToString();
  }
  catch(Exception ex) {
    string message = 
      String.Format("Something went wrong serializing DTO {0}", DTO);
    throw new MyLibraryException(message, ex);
  }
}

Gracias, sí, el envoltorio de excepción (especialmente encadenado) es perfectamente sano ... lo que no es sano es atrapar una excepción solo para que puedas tirar el rastro de la pila, o peor aún, comértelo.
corlettk

10

¿No es esto exactamente equivalente a no manejar excepciones en absoluto?

No exactamente, no es lo mismo. Restablece el stacktrace de la excepción. Aunque estoy de acuerdo en que probablemente sea un error y, por lo tanto, un ejemplo de código incorrecto.


8

No quieres tirar ex, ya que esto perderá la pila de llamadas. Consulte Manejo de excepciones (MSDN).

Y sí, el intento ... catch no está haciendo nada útil (aparte de perder la pila de llamadas, por lo que en realidad es peor, a menos que por alguna razón no quiera exponer esta información).


No pierde toda la pila de llamadas cuando usa throw ex, solo pierde la parte de la pila de llamadas desde el punto donde la excepción ocurrió más arriba en su pila de llamadas. Pero retiene la pila de llamadas del método que arrojó la excepción a donde la llamó el cliente. En realidad, podría haber casos en los que usaría eso, o de lo contrario las buenas personas de Microsoft no lo habrían permitido. Dicho eso, no lo he usado. Otra cuestión para recordar es que lanzar excepciones es costoso. Solo hazlo por una razón muy justificable. El registro creo que sería justificable, etc.
Charles Owen

5

Un punto que la gente no ha mencionado es que, si bien los lenguajes .NET realmente no hacen una distinción adecuada, la cuestión de si uno debe tomar medidas cuando se produce una excepción y si se resolverá , en realidad son preguntas distintas. Hay muchos casos en los que uno debe tomar medidas basadas en excepciones que uno no tiene la esperanza de resolver, y hay algunos casos en los que todo lo que es necesario para "resolver" una excepción es desenrollar la pila hasta cierto punto; no se requieren acciones adicionales .

Debido a la sabiduría común de que uno solo debe "atrapar" cosas que uno puede "manejar", una gran cantidad de código que debe tomar medidas cuando ocurren excepciones, no lo hace. Por ejemplo, una gran cantidad de código adquirirá un bloqueo, colocará el objeto protegido "temporalmente" en un estado que viola sus invariantes, lo colocará en un estado legítimo y luego liberará el bloqueo antes de que alguien más pueda ver el objeto. Si ocurre una excepción mientras el objeto está en un estado peligrosamente inválido, la práctica común es liberar el bloqueo con el objeto aún en ese estado. Un patrón mucho mejor sería tener una excepción que ocurra mientras el objeto está en una condición "peligrosa" que invalida expresamente el bloqueo, por lo que cualquier intento futuro de adquirirlo fallará de inmediato.

En la mayoría de los lenguajes .NET, la única forma de que el código tome medidas basadas en una excepción es catchhacerlo (aunque sepa que no va a resolver la excepción), realice la acción en cuestión y luego vuelva a ejecutarla throw. Otro enfoque posible si al código no le importa qué excepción se produce es usar una okbandera con un try/finallybloque; establezca la okbandera falseantes del bloque, y trueantes de que el bloque salga, y antes de cualquiera returnque esté dentro del bloque. Luego, dentro finally, suponga que si okno está configurado, debe haber ocurrido una excepción. Tal enfoque es semánticamente mejor que a catch/ throw, pero es feo y es menos sostenible de lo que debería ser.


5

Esto puede ser útil cuando su programación funciona para una biblioteca o dll.

Esta estructura de repetición puede usarse para restablecer la pila de llamadas a propósito, de modo que en lugar de ver la excepción lanzada desde una función individual dentro de la función, obtenga la excepción de la función misma.

Creo que esto solo se usa para que las excepciones lanzadas sean más claras y no entren en las "raíces" de la biblioteca.


3

Una posible razón para atrapar es deshabilitar los filtros de excepción que se encuentran más arriba en la pila para que no se filtren ( enlace antiguo aleatorio ). Pero, por supuesto, si esa fuera la intención, habría un comentario allí que lo diga.


No entendí de qué estabas hablando hasta que leí el enlace ... y todavía no estoy exactamente seguro de qué estás hablando ... de que no estoy totalmente familiarizado con VB.NET. Creo que da como resultado que la suma sea reportada como "inconsistente", ¿verdad? ... Soy un GRAN fanático de los métodos estáticos ... aparte de que son simples, hay menos posibilidades de inconsistencia si separas la configuración de atributos del código que hace el trabajo real La pila es "autolimpiante".
corlettk

3
La gente espera que cuando escriben "intente {Foo ();} finalmente {Bar ();}" que nada funcione entre Foo y Bar. Pero esto no es cierto; si su interlocutor agregó un filtro de excepción, y no hay 'captura' interviniente, y Foo () lanza, entonces se ejecutará algún otro código aleatorio de su interlocutor antes de que finalmente se ejecute su (Bar). Esto es muy malo si ha roto invariantes o ha aumentado la seguridad, esperando que finalmente sean restaurados 'inmediatamente' a la normalidad y ningún otro código verá el cambio temporal.
Brian

3

Depende de lo que esté haciendo en el bloque catch, y si desea pasar el error al código de llamada o no.

Puede decir Catch io.FileNotFoundExeption exy luego usar una ruta de archivo alternativa o algo así, pero aún así arrojar el error.

También haciendo en Throwlugar deThrow Ex permite mantener el seguimiento completo de la pila. Throw ex reinicia el seguimiento de la pila desde la instrucción throw (espero que tenga sentido).


3

Si bien muchas de las otras respuestas brindan buenos ejemplos de por qué es posible que desee capturar una repetición de una excepción, nadie parece haber mencionado un escenario 'finalmente'.

Un ejemplo de esto es cuando tiene un método en el que establece el cursor (por ejemplo, un cursor de espera), el método tiene varios puntos de salida (por ejemplo, si () regresa;) y desea asegurarse de que el cursor se restablezca en el Fin del método.

Para hacer esto, puede envolver todo el código en un intento / captura / finalmente. Finalmente, vuelva a colocar el cursor en el cursor derecho. Para que no entierres ninguna excepción válida, vuelve a lanzarla en la captura.

try
{
    Cursor.Current = Cursors.WaitCursor;
    // Test something
    if (testResult) return;
    // Do something else
}
catch
{
    throw;
}
finally
{
     Cursor.Current = Cursors.Default;
}

1
¿Fue catchuna parte obligatoria try...finallyhistóricamente o desempeña un papel funcional en este ejemplo? - Solo verifiqué dos veces, y puedo usarlo try {} finally {}sin el bloqueo.
Sebi

2

En el ejemplo en el código que ha publicado, de hecho, no tiene sentido capturar la excepción, ya que no se hace nada en la captura, solo se vuelve a mostrar, de hecho, hace más daño que bien ya que se pierde la pila de llamadas .

Sin embargo, detectaría una excepción para hacer algo de lógica (por ejemplo, cerrar la conexión sql del bloqueo de archivos, o simplemente un poco de registro) en caso de una excepción, devolverla al código de llamada para tratar. Esto sería más común en una capa empresarial que en el código front end, ya que es posible que desee que el codificador que implementa su capa empresarial maneje la excepción.

Para volver a iterar aunque no hay ningún punto en la captura de la excepción en el ejemplo que publicó. ¡NO lo hagas así!


1

Lo sentimos, pero muchos ejemplos como "diseño mejorado" todavía huelen horriblemente o pueden ser extremadamente engañosos. Tener intente {} catch {log; throw} es completamente inútil. El registro de excepciones debe hacerse en un lugar central dentro de la aplicación. de todos modos, las excepciones aumentan la traza de la pila, ¿por qué no registrarlas en algún lugar cerca de los bordes del sistema?

Se debe tener precaución cuando se serializa su contexto (es decir, DTO en un ejemplo dado) solo en el mensaje de registro. Puede contener fácilmente información confidencial que quizás no desee alcanzar las manos de todas las personas que pueden acceder a los archivos de registro. Y si no agrega ninguna información nueva a la excepción, realmente no veo el punto de ajuste de excepción. El viejo Java tiene algún punto para eso, requiere que la persona que llama sepa qué tipo de excepciones se deben esperar luego de llamar al código. Como no tiene esto en .NET, el ajuste no sirve de nada en al menos el 80% de los casos que he visto.


Gracias por tu pensamiento, Joe. En Java (y C #, supongo), me encantaría ver una anotación de nivel de clase @FaultBoundary que obliga a TODAS las excepciones (incluidos los tipos de excepción no verificados) a ser capturadas o declaradas para ser lanzadas. Usaría esta anotación en las interfaces públicas de cada capa arquitectónica. Por lo tanto, la interfaz @FaultBoundary ThingDAO no podría filtrar detalles de implementación como SQLExceptions, NPE o AIOB. En su lugar, se registraría el seguimiento de pila "causal" y se generaría una DAOSystemException ... Defino la excepción del sistema como "permanentemente fatal".
corlettk 03 de

55
Hay muchas razones para atrapar, registrar y volver a lanzar. Específicamente, si el método con el registro de captura tiene información que pierde una vez que está fuera del método. El error puede manejarse más tarde pero no registrarse, y ha perdido información sobre defectos en el sistema.
Andy

1
Aquí es donde la propiedad Data de la clase Exception es útil: captura toda la información local para el registro genérico. Este artículo me llamó la atención originalmente: blog.abodit.com/2010/03/…
McGuireV10

1

Además de lo que han dicho los demás, vea mi respuesta a una pregunta relacionada que muestra que atrapar y volver a lanzar no es un no-op (está en VB, pero parte del código podría ser invocado en C # desde VB).


Si bien este enlace puede responder la pregunta, es mejor incluir aquí las partes esenciales de la respuesta y proporcionar el enlace como referencia. Las respuestas de solo enlace pueden volverse inválidas si la página vinculada cambia. - De la opinión
LHIOUI

@HamzaLH, estoy de acuerdo en que no es una respuesta bien escrita, pero tiene información, diferente a otras respuestas y votos positivos. Entonces no entiendo, ¿por qué sugieres eliminarlo? "Las respuestas cortas sobre el tema y que dan una solución siguen siendo respuestas". De meta.stackexchange.com/questions/226258/…
Michael Freidgeim

esta es una respuesta de solo enlace
LHIOUI

1. Las respuestas de solo enlace deben cambiarse a comentarios, no eliminarse. 2. Es una referencia a otra pregunta SO, no al sitio externo, que se considera menos probable que se rompa con el tiempo. 3. Tiene una descripción adicional, que hace que no sea "solo enlace" - ver meta.stackexchange.com/questions/225370/…
Michael Freidgeim

1

La mayoría de las respuestas hablan del escenario catch-log-rethrow.

En lugar de escribirlo en su código, considere usar AOP, en particular Postsharp.Diagnostic.Toolkit con OnExceptionOptions IncludeParameterValue e IncludeThisArgument


Si bien este enlace puede responder la pregunta, es mejor incluir aquí las partes esenciales de la respuesta y proporcionar el enlace como referencia. Las respuestas de solo enlace pueden volverse inválidas si la página vinculada cambia. - De la opinión
Tony Dong

@ TonyDong, estoy de acuerdo en que no es una respuesta bien escrita, pero tiene información, diferente a otras respuestas y votos positivos. Entonces no entiendo, ¿por qué sugieres eliminarlo? Por cierto, el enlace 5 años después sigue siendo válido. "Las respuestas cortas sobre el tema y que dan una solución siguen siendo respuestas". De meta.stackexchange.com/questions/226258/…
Michael Freidgeim

Stackoverflow solo tiene esta sugerencia.
Tony Dong

@TonyDong, si la respuesta no es absolutamente inútil, debe elegir "Se ve bien"
Michael Freidgeim

0

Volver a generar excepciones a través de throwes útil cuando no tiene un código particular para manejar las excepciones actuales, o en los casos en que tiene una lógica para manejar casos de error específicos pero desea omitir todos los demás.

Ejemplo:

string numberText = "";
try
{
    Console.Write("Enter an integer: ");
    numberText = Console.ReadLine();
    var result = int.Parse(numberText);

    Console.WriteLine("You entered {0}", result);
}
catch (FormatException)
{
    if (numberText.ToLowerInvariant() == "nothing")
    {
        Console.WriteLine("Please, please don't be lazy and enter a valid number next time.");
    }
    else
    {
        throw;
    }
}    
finally
{
    Console.WriteLine("Freed some resources.");
}
Console.ReadKey();

Sin embargo, también hay otra forma de hacerlo, utilizando cláusulas condicionales en bloques catch:

string numberText = "";
try
{
    Console.Write("Enter an integer: ");
    numberText = Console.ReadLine();
    var result = int.Parse(numberText);

    Console.WriteLine("You entered {0}", result);
}
catch (FormatException) when (numberText.ToLowerInvariant() == "nothing")
{
    Console.WriteLine("Please, please don't be lazy and enter a valid number next time.");
}    
finally
{
    Console.WriteLine("Freed some resources.");
}
Console.ReadKey();

Este mecanismo es más eficiente que volver a lanzar una excepción porque el tiempo de ejecución de .NET no tiene que reconstruir el objeto de excepción antes de volver a lanzarlo.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.