.net excepciones - ¿Cuál es la forma correcta de volver a lanzar una excepción en C#?




para tipos (9)

Esta pregunta ya tiene una respuesta aquí:

Tengo una pregunta para usted que proviene de que mi pareja hace las cosas de una manera diferente a la que yo hago.

¿Es mejor hacer esto?

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

o esto:

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

¿Hacen lo mismo? ¿Es uno mejor que el otro?


Answers

El primero conserva el rastro de pila original de la excepción, el segundo lo reemplaza con la ubicación actual.

Por lo tanto, el primero es, por mucho, mejor.


Mis preferencias es usar

try 
{
}
catch (Exception ex)
{
     ...
     throw new Exception ("Put more context here", ex)
}

Esto conserva el error original, pero le permite poner más contexto, como un ID de objeto, una cadena de conexión, cosas así. A menudo, mi herramienta de informes de excepciones tendrá 5 excepciones encadenadas al informe, cada una con más detalles.


Siempre debe usar la siguiente sintaxis para volver a generar una excepción; de lo contrario, pisará el seguimiento de pila:

throw;

Si imprime la traza resultante de "throw ex", verá que termina en esa declaración y no en la fuente real de la excepción.

Básicamente, debe considerarse un delito penal usar "lanzar ex".


Sé que esta es una pregunta antigua, pero voy a responderla porque tengo que estar en desacuerdo con todas las respuestas aquí.

Ahora, estoy de acuerdo en que la mayoría de las veces desea hacer una throw simple, para preservar la mayor cantidad de información posible acerca de lo que salió mal, o quiere lanzar una nueva excepción que pueda contener eso como una excepción interna, o no, según la probabilidad de que quiera saber sobre los eventos internos que lo causaron.

Aunque hay una excepción. Hay varios casos en los que un método llamará a otro método y una condición que causa una excepción en la llamada interna debe considerarse la misma excepción en la llamada externa.

Un ejemplo es una colección especializada implementada usando otra colección. Digamos que es una List<T> DistinctList<T> que envuelve una List<T> pero rechaza los elementos duplicados.

Si alguien llamó a ICollection<T>.CopyTo en su clase de colección, podría ser simplemente una llamada directa a CopyTo en la colección interna (por ejemplo, toda la lógica personalizada solo se aplicó para agregar a la colección o configurarla). Ahora, las condiciones en las que se ICollection<T>.CopyTo esa llamada son exactamente las mismas condiciones en las que su colección debería coincidir con la documentación de ICollection<T>.CopyTo .

Ahora, simplemente no podría captar la exención en absoluto, y dejarla pasar. Sin embargo, aquí el usuario obtiene una excepción de la List<T> cuando estaba llamando a algo en una DistinctList<T> . No es el fin del mundo, pero es posible que desee ocultar los detalles de la implementación.

O usted podría hacer su propia comprobación:

public CopyTo(T[] array, int arrayIndex)
{
  if(array == null)
    throw new ArgumentNullException("array");
  if(arrayIndex < 0)
    throw new ArgumentOutOfRangeException("arrayIndex", "Array Index must be zero or greater.");
  if(Count > array.Length + arrayIndex)
    throw new ArgumentException("Not enough room in array to copy elements starting at index given.");
  _innerList.CopyTo(array, arrayIndex);
}

Ese no es el peor código porque está escrito y es probable que podamos copiarlo desde alguna otra implementación de CopyTo donde no fuera un simple paso y tuvimos que implementarlo nosotros mismos. Sin embargo, está repitiendo innecesariamente las mismas comprobaciones que se realizarán en _innerList.CopyTo(array, arrayIndex) , por lo que lo único que ha agregado a nuestro código es 6 líneas donde podría haber un error.

Podríamos revisar y envolver:

public CopyTo(T[] array, int arrayIndex)
{
  try
  {
    _innerList.CopyTo(array, arrayIndex);
  }
  catch(ArgumentNullException ane)
  {
    throw new ArgumentNullException("array", ane);
  }
  catch(ArgumentOutOfRangeException aore)
  {
    throw new ArgumentOutOfRangeException("Array Index must be zero or greater.", aore);
  }
  catch(ArgumentException ae)
  {
    throw new ArgumentException("Not enough room in array to copy elements starting at index given.", ae);
  }
}

En términos de nuevo código agregado que podría tener errores, esto es aún peor. Y no ganamos nada de las excepciones internas. Si pasamos una matriz nula a este método y recibimos una ArgumentNullException , no vamos a aprender nada examinando la excepción interna y _innerList.CopyTo que una llamada a _innerList.CopyTo recibió una matriz nula y lanzó una ArgumentNullException .

Aquí, podemos hacer todo lo que queramos con:

public CopyTo(T[] array, int arrayIndex)
{
  try
  {
    _innerList.CopyTo(array, arrayIndex);
  }
  catch(ArgumentException ae)
  {
    throw ae;
  }
}

Cada una de las excepciones que esperamos tener que lanzar si el usuario lo llama con argumentos incorrectos, será lanzada correctamente por ese relanzamiento. Si hay un error en la lógica que se usa aquí, está en una de dos líneas: o bien nos equivocamos al decidir que este fue un caso en el que funciona este enfoque, o nos equivocamos al tener la excepción ArgumentException como el tipo de excepción buscado. Son los dos únicos errores que el bloque catch puede tener.

Ahora. Todavía estoy de acuerdo en que la mayoría de las veces, o bien quieres un throw; sencillo throw; o quiere construir su propia excepción para que coincida más directamente con el problema desde la perspectiva del método en cuestión. Hay casos como el anterior, donde volver a lanzar de esta manera tiene más sentido, y hay muchos otros casos. Por ejemplo, para tomar un ejemplo muy diferente, si un lector de archivos ATOM implementado con un FileStream y un XmlTextReader recibe un error de archivo o un XML no válido, tal vez querrá lanzar exactamente la misma excepción que recibió de esas clases, pero debería tener en cuenta la persona que llama que es AtomFileReader está lanzando una FileNotFoundException o XmlException , por lo que podrían ser candidatos para relanzar de manera similar.

Editar:

También podemos combinar los dos:

public CopyTo(T[] array, int arrayIndex)
{
  try
  {
    _innerList.CopyTo(array, arrayIndex);
  }
  catch(ArgumentException ae)
  {
    throw ae;
  }
  catch(Exception ex)
  {
    //we weren't expecting this, there must be a bug in our code that put
    //us into an invalid state, and subsequently let this exception happen.
    LogException(ex);
    throw;
  }
}

Descubrí que si la excepción se produce en el mismo método que se captura, el seguimiento de pila no se conserva, por lo que vale.

void testExceptionHandling()
{
    try
    {
        throw new ArithmeticException("illegal expression");
    }
    catch (Exception ex)
    {
        throw;
    }
    finally
    {
        System.Diagnostics.Debug.WriteLine("finally called.");
    }
}

El primero es mejor. Si intenta depurar el segundo y mira la pila de llamadas, no verá de dónde proviene la excepción original. Hay trucos para mantener intacta la pila de llamadas (intente buscar, se ha respondido antes) si realmente necesita volver a lanzar.


Si lanza una excepción sin una variable (el segundo ejemplo), StackTrace incluirá el método original que lanzó la excepción.

En el primer ejemplo, el StackTrace se cambiará para reflejar el método actual.

Ejemplo:

static string ReadAFile(string fileName) {
    string result = string.Empty;
    try {
        result = File.ReadAllLines(fileName);
    } catch(Exception ex) {
        throw ex; // This will show ReadAFile in the StackTrace
        throw;    // This will show ReadAllLines in the StackTrace
    }

Depende. En una compilación de depuración, quiero ver el seguimiento de la pila original con el menor esfuerzo posible. En ese caso, "tirar"; encaja a la perfección. Sin embargo, en una versión de lanzamiento, (a) quiero registrar el error con el seguimiento de pila original incluido, y una vez hecho esto, (b) remodelar el manejo de errores para que tenga más sentido para el usuario. Aquí "Lanzar Excepción" tiene sentido. Es cierto que volver a generar el error descarta el seguimiento de la pila original, pero un desarrollador no obtiene nada al ver la información de seguimiento de la pila, por lo que está bien volver a emitir el error.

        void TrySuspectMethod()
        {
            try
            {
                SuspectMethod();
            }
#if DEBUG
            catch
            {
                //Don't log error, let developer see 
                //original stack trace easily
                throw;
#else
            catch (Exception ex)
            {
                //Log error for developers and then 
                //throw a error with a user-oriented message
                throw new Exception(String.Format
                    ("Dear user, sorry but: {0}", ex.Message));
#endif
            }
        }

La forma en que está redactada la pregunta, que enfrenta a "Throw:" frente a "Throw ex;" hace que sea un poco de arenque rojo. La elección real es entre "tirar"; y "Lanzar excepción", donde "Lanzar ex"; Es un caso especial poco probable de "Excepción de tiro".


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

Debido a la creencia común de que solo se debe "atrapar" las cosas que se pueden "manejar", muchos códigos que deben actuar cuando se producen excepciones, no lo hacen. Por ejemplo, una gran cantidad de código adquirirá un bloqueo, colocará el objeto protegido "temporalmente" en un estado que viola sus invariantes, luego lo pondrá 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 se encuentra 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", invalida expresamente el bloqueo, por lo que cualquier intento futuro de adquirirlo fallará de inmediato. El uso consistente de tal patrón mejoraría en gran medida la seguridad del llamado manejo de excepciones "Pokémon", que en mi humilde opinión adquiere una mala reputación principalmente debido al código que permite que se realicen excepciones sin tomar las medidas apropiadas primero.

En la mayoría de los lenguajes .NET, la única manera de que el código tome medidas en función de una excepción es catch (aunque sabe que no va a resolver la excepción), realizar la acción en cuestión y luego volver a throw ). Otro enfoque posible si al código no le importa qué excepción se lanza es usar un indicador ok con un bloque try/finally ; establezca el indicador ok en false antes del bloqueo y en true antes de que salga el bloque, y antes de cualquier return que esté dentro del bloque. Luego, finally , supongamos que si no se configura ok , debe haber ocurrido una excepción. Tal enfoque es semánticamente mejor que un catch / throw , pero es feo y es menos mantenible de lo que debería ser.







c# .net exception-handling