Tratando con excepciones

by Valeriano Tortola 9. diciembre 2007 21:19

Buff... cuanto tiempo desde mi último post, entre estudiar y la medio-gripe que arrastro desde hace unas semanas no tengo ganas tiempo de nada :P

Un tema que siempre me ha parecido que se deja un poco olvidado a la hora de desarrollar una aplicación es el tema de las excepciones, su flujo, su registro y como afectan al diseño y modularidad de la aplicación.

Una excepción, es un error en el flujo lógico ideal de nuestra aplicación causado por una condición inusual. Ante un error de este tipo, debemos actuar para corregirlo y/ó recuperar un estado consistente ó registrarlo en algún lugar(archivo log, EventLog, ..etc..) donde el administrador pueda ver que ha sucedido y tomar medidas. Esto es algo bien sabido por todos, pero el problema viene cuando hay que decidir que hacer con las excepciones que no podemos solucionar y como registrar de forma descriptiva el problema para poder solventarlo.

El objetivo de capturar una excepción, es enfrentarse a una situación inesperada, por lo que debemos capturar las excepciones que podamos solucionar y las que no, dejarlas subir por la pila de llamadas hasta el punto donde se registren como un error no controlado. Por este motivo, esta ampliamente desaconsejado capturar de forma generalizada:

      try
      {
        // lógica...
      }
      catch (Exception)
      {
        // Todas las excepciones son capturadas..
      }

Sin embargo, hay situaciones donde es vital capturar la excepción aún cuando no se puede hacer nada para contrarrestarla, por ejemplo cuando usamos transacciones, es vital capturarla para deshacer las operaciones realizadas. En estos casos, una vez tomadas las acciones oportunas podemos re-lanzarla de nuevo:

      MessageQueueTransaction tran = new MessageQueueTransaction();
      try
      {
        tran.Begin();
 
        // Trabajar en el contexto de la transacción
 
        tran.Commit();
      }
      catch (Exception)
      {
        // Ha habido problemas, realizamos un rollback
        tran.Abort();
 
        // Re-lanzamos la excepción para que sea tratada
        // en capas superiores.
        throw;
      }
      finally
      {
        tran.Dispose();
      }

Antaño (VB6, ASP,..etc...) era necesario capturar la excepción allí donde ocurriese para poder registrar el punto donde sucedió, pero las excepciones de .NET Framework (que derivan todas de la clase Exception) contienen la propiedad StackTrace que registra la consecución de llamadas que han sucedido en la pila hasta toparse con dicha excepción. Sabiendo esto, no necesitamos poner bloques try-catch en cada una de nuestras funciones para saber el lugar exacto donde sucedió.

Cuando se vaya a re-lanzar una excepción es importante hacerlo simplemente con "throw;" ya que preserva la información del StackTraceNO como el siguiente ejemplo, ya que se perdería el  StackTrace original:

      catch (Exception ex)
      {
        // Ha habido problemas, realizamos un rollback
        tran.Abort();
 
        // MAL: Así perdemos el StackTrace original.
        // Sería más difícil encontrar el problema.
        throw ex;
      }

Cuando se quiera usar tipos de excepciones propios ó simplemente otro para dar mensajes de error más descriptivos y/ó completos, podemos usar la propiedad InnerException para poder mantener la información original del error:

catch (ArgumentNullException ae)
{
  throw new ArgumentException("El objeto no era válido", ae);
}

De esta forma, se mantiene la información del error original.

No se debe utilizar las excepciones como elementos de nuestra lógica y debemos evadirlas en la medida de lo posible ya que el uso de excepciones implica una penalización de rendimiento. Cuando digo evadirlas, me refiero a que si esperamos un error muy concreto... mejor procurar que el error no suceda, por ejemplo:

static Int32 Divide(Int32 a, Int32 b)
{
  Int32 result = 0;
  try
  {
    result = a / b;
  }
  catch (DivideByZeroException)
  {
    result = -1;
  }
  return result;
}

En este sencillo ejemplo, contemplamos la posibilidad de que la variable 'b' sea 0 y una excepción de tipo DivideByZeroException ocurra. Aunque se toman medidas para devolver un posible valor inocuo, se podría evitar fácilmente:

static Int32 Divide(Int32 a, Int32 b)
{
  Int32 result = 0;
  if (b == 0) return -1;
  result = a / b;
  return result;
}

El como registrar las excepciones no controladas es algo ya más dependiente de los requerimientos del proyecto ó conveniencia técnica, se puede usar desde un simple archivo de texto, el EventLog ó por ejemplo el Logging Application Block de Enterprise Library 3.1 (del que espero escribir en breve... si no las pincho antes xD).

Como esto se hace largo ya... en un próximo post escribiré algo sobre donde y porque capturar excepciones de forma que no afecte a la modularidad de nuestra aplicación.

Links:

Tags:

.NET 2.0 | C# 2.0

Comentarios

10/12/2007 0:24:11 #

trackback

Trackback from Pensando en asíncrono

Tratando con excepciones

Pensando en asíncrono |

10/12/2007 0:26:01 #

trackback

Trackback from vtortola

Tratando con excepciones

vtortola |

10/12/2007 18:46:54 #

Cristhian

En realidad el comentario es por tocar un poco las narices, y por ver si al menos así hay algún enlace externo a mi blog (sí, ya lo sé. es patético)

Cuando usamos un bloque try..catch para controlar una transacción, en el ejemplo que has puesto, lo suyo es usar un catch a secas, sin el Exception ex, ya que si no haces nada con la variable, no tiene sentido instanciarla. Además, el compilador te mostrará un warning.

La cosa quedaría así:
            try
            {
                // Configurar command
                // ...
                dc.Transaction = trans;
                // Ejecutar
                dc.ExecuteNonQuery();
                // Commit
                trans.Commit();
            }<B>
            catch
            {
                // RollBack
                trans.Rollback();
                throw;
            }</B>

Puedes ver el ejemplo completo en:
cs.crisfervil.com/.../...esumen-ado-net-y-xml.aspx

Salu2


Cristhian España |

10/12/2007 21:46:31 #

vtortola

Si y no Laughing

Si lees bien el ejemplo de la transacción, no he puesto "catch(Exception ex)" sino "catch(Exception)", esto no crea una instancia, simplemente define un filtro. Pero aún así, "catch(Exception ex)" tampoco crea una instancia, la instacia se crea en el mismo momento que la excepción se produce, el hacer "catch(Exception ex)" solo define una variable para poder acceder al objeto Exception que se ha creado a raiz del error. Uses una cosa u otra la instancia existe igual ;)

La diferencia entre "catch(Exception)" y "catch" a secas es que el primero solo "captura" las excepciones administradas mientras que el segundo captura las administradas y las no administradas (non-CLS), es decir, "catch" a secas se lo come todo Laughing es un método para filtrar. En un entorno full-managed no tiene sentido definir un filtro tan amplio... bajo mi gusto claro, aunque si en la transación juegan elementos no administrados como COM+ en un MSDTC, habría que indicarlo como tu dices, con un "catch" pelado.

Evidentemente el código era para mostrar el ejemplo Smile

Un saludo.

vtortola España |

13/12/2007 22:22:49 #

espinete

Hola,
se podría poner algo así:

catch (Exception ex)
{
   // para administrada
}
catch
{
   // para no administradas
}

saludos.

espinete |

09/01/2008 20:06:53 #

casas

Yo no entiendo muy bien todo esto de lo que hablais, estudie algo, pero vais a un nivel superior no?

casas |

Comentarios no permitidos