viernes, 16 de mayo de 2008

Sobre excepciones en .net

Excepciones

El framework de .net utiliza un mecanismo para informar de situaciones que impiden el flujo normal del código como un error, se trata de las excepciones.

 Las excepciones se pueden producir por muchas razones, en general situaciones inesperadas, por ejemplo querer acceder a un archivo que no existe, esto produce una horrible excepción, veamos un poco de código

using (StreamReader reader = new StreamReader(@"c:\archivoQueNoExiste.txt"))
{
 reader.ReadLine();
 reader.Close();
}

 Si lo ejecutamos recibimos una fea fea excepción del tipo FileNotFoundException, ok esto es lo que queríamos, ahora bien, qué hacemos ahora?

Alternativa ante las excepciones

Inicialmente podemos hacer dos cosas cuando nos encontramos con una porción de código que puede lanzar una excepción

-Atraparla en un bloque catch

-No hacer nada

Comencemos por el primer caso

try 
{ 
 using (StreamReader reader = new StreamReader(@"c:\archivoQueNoExiste.txt")) 
 { 
  reader.ReadLine();
  reader.Close();
 } 
}catch 
{ 
 throw;
} 

Qué pasa acá, bien, lo mismo que no hacer nada, porque en el bloque volvemos a lanzar la excepción por lo tanto es lo mismo que no hacer nada

try 
{ 
 using (StreamReader reader = new StreamReader(@"c:\archivoQueNoExiste.txt")) 
 { 
 reader.ReadLine();
 reader.Close();
 } 
}catch(Exception ex) 
{ 
 throw ex;
} 

Este ejemplo es peor que el anterior porque no sólo no hacemos nada sino que además al colocar Exeception ex y luego hacer throw borramos toda la información que se recopiló hasta el momento del error (el stack trace) por lo tanto, no sólo no la manipulamos sino que además perdemos valiosa información.

Enseñanza:

-Si atrapamos una excepción y la relanzamos haciendo referencia al objeto Exception borramos el stack trace.

try 
{ 
 using (StreamReader reader = new StreamReader(@"c:\archivoQueNoExiste.txt")) 
 { 
  reader.ReadLine();
  reader.Close();
 } 
}catch(Exception ex) 
{ 
 GuardarInformacionDelError(ex);
} 

Esto es bastante más elegante, ocurre un error y guardamos la información para un análisis posterior, sin embargo esto nos lleva a una enseñanza mejor, por qué no hacer mejor esto:

string nombre = @"c:\archivoQueNoExiste.txt";
if (File.Exists(nombre))
{ 
 using (StreamReader reader = new StreamReader(nombre)) 
 { 
  reader.ReadLine();
  reader.Close();
 }
}

 Nada de nada, no ocurre la excepción porque estamos verificando de antemano la existencia del archivo, la enseñanza es que es mejor evitar las excepciones, porque son costosas a nivel recursos y son feas también.

 Manejando excepciones

En caso de querer manejar la excepción o que ocurra una excepción inesperada por ejemplo:

string nombre = @"c:\archivoQueNoExiste.txt";
if (File.Exists(nombre))
{
 using (StreamWriter writer = new StreamWriter(nombre))
 { 
  writer.Write("hola mundo");
  writer.Close();
 }
}

 y que el archivo sea de sólo lectura o algo así (obviamente también podemos verificarlo antes, es un ejemplo), entonces tenemos un bloque de código en el catch para manipular la excepción, pero qué hacemos? Primero que nada podemos guardar información de error, segundo lanzar otra excepción, una personalizada, indicando un problema con información más "amigable"

try 
{
 string nombre = @"c:\archivoQueNoExiste.txt";
 if (File.Exists(nombre)) 
 {
  using (StreamWriter writer = new StreamWriter(nombre))
  {
   writer.Write("hola mundo"); 
   writer.Close();
  }
 }
}catch (Exception ex) 
{
 GuardarInformacionDelError(ex);
 throw new ExcepcionPersonalizada("Ocurrión un error sentimos las molestias");
 }

 Un lujo asiático, vamos a hacer un resumen de lo que aprendimos

-Siempre es mejor verificar para evitar las excepciones que esperar que ocurran

-Si ocurren y vamos a atraparlas hay que manipularlas correctamente, de muy poco sirve relanzar la misma excepción

Tipificando excepciones

Las excepciones son objeto de una clase, todas heredan de la clase base Exception y las podemos diferenciar por el tipo, nada de códigos de error ni cosas raras

Esta tipificación nos permite poder hacer cosas simpáticas filtrando las excepciones, veámos el siguiente ejemplo:

try 
{ 
 string nombre = @"c:\archivoQueNoExiste.txt";
 if (File.Exists(nombre)) 
 {
  using (StreamWriter writer = new StreamWriter(nombre))
  {
   writer.Write("hola mundo"); 
   writer.Close();
  }
 }
}catch (UnauthorizedAccessException)
{
 throw new ExcepcionPersonalizada("No se puede acceder al archivo");
}catch (Exception ex) 
{ 
 GuardarInformacionDelError(ex);
 throw new ExcepcionPersonalizada("Ocurrió un error inesperado"); 
}

 En este caso en caso de ocurrir una excepcion del tipo UnauthorizedAccessException vamos a lanzar otra indicando que no se puede acceder al achivo, ahora cuando ocurra cualquier otra excepción se va a guardar la información y se va a lanzar una excepción personalizada indicando que pasó algo raro. Ahora, sabemos que UnautorizedAccessException hereda de SystemException qué pasa si hacemos esto?:

try 
{ 
 string nombre = @"c:\archivoQueNoExiste.txt";
 if (File.Exists(nombre)) 
 {
  using (StreamWriter writer = new StreamWriter(nombre))
  {
   writer.Write("hola mundo"); writer.Close();
  }
 }
}catch (UnauthorizedAccessException)
{
 throw new ExcepcionPersonalizada("No se puede acceder al archivo"); 
}catch (SystemException)
{
 throw new ExcepcionPersonalizada("Error de sistema");
}catch (Exception ex)
{
 GuardarInformacionDelError(ex);
 throw new ExcepcionPersonalizada("Ocurrió un error inesperado");
}

 Bien, se ejecuta el bloque que tiene UnauthorizedAccessException porque siempre se ejecuta primero el bloque con el tipo más especializado, es decir, como la excepción es del tipo UnauthorizedAccessException por más que esta herede de SystemException se ejecuta su bloque para que podamos hacer algo con ese tipo en particuar de excepción, otra cosa con todas las otras excepciones que hereden de systemException y por útlimo otra cosa con el resto.

Otra enseñanza

-Cuando tenemos varios bloque catch se ejecuta el que tiene el tipo que mejor encaja con el de la excepción que se produjo y sólo ese.

 Ahora bien, qué pasa si hacemos esto:

try 
{
 string nombre = @"c:\archivoQueNoExiste.txt";
 if (File.Exists(nombre))
 {
  using (StreamWriter writer = new StreamWriter(nombre))
  {
   writer.Write("hola mundo"); writer.Close();
  }
 }
}catch (ApplicationException)
{
 throw new ExcepcionPersonalizada("No se puede acceder al archivo");
}

 Nadie atrapa la excepción porque estamos sólo teniendo en cuenta las del tipo ApplicationException y deribadas y no es el caso de UnauthorizedAccessException, entonces la excepción "burbujea" al método que invocó al actual y seguirá haciendo esto hasta que algún bloque maniuple la excepción, si esto nunca ocurre la aplicación pincha, o más técnicamente, el CLR la finaliza con un carte muy feo que habla acerca de nuestro descuído.

Otra enseñanza

-Si no atrapamos una excepción esta burbujea a la capa superior y así sucesivamente hasta que alguien la manipula o el CLR interrumpe la aplicación

No utilizar excepciones como control de flujo para reglas de negocios

Un tema polémico, no es recomendable utilizar excepciones personalizadas para controlar el flujo de la aplicación por dos razones:

-Son costosas a nivel recursos

-Y se llaman excepciones porque deben ocurrir sólo en ocasiones excepcionales

Auque puede haber casos en los que se utilicen, por ejemplo, escribimor un servicio web para que consuma alquien a través de internet, poco seguro sería si la validación de los datos del usuario devolviese un valor booleano si la autenticación del mismo fuera errónea, creo que más de uno pensaría en que es sencillo saltarla, en este caso lo más recomendable es lanzar una excepción para que se interrumpa la comunicación directamente.

Crear excepciones personalizadas

Bueno, no hay mucho para decir, simplemente hay que seguir una regla, que nuestras excepciones hereden de ApplicationException porque Exception está reservada para excepciones del CLR, esto nos va a permitir un mejor filtrado, más aún si definimos nuestra propia excepción base y hacemos heredas todas nuestras excepciones de esta.

Hasta la próxima.

3 comentarios:

Javier Carmona dijo...

Muchas gracias por esta contribución, no solo resolví mi problema sino aprendi (y me reí) con tu forma de compartir conocimiento, gracias y espero encontrarnos de nuevo

Leonardo Micheloni dijo...

Hola Javier,
Gracias por el comentario, la idea es devolver un poco de todo lo que recibo de otros bloggers y en castellano para la comunidad hispana, me alegro que te haya sido útil, un abrazo.

Roger dijo...

Muchas gracias,

Tu explicación me ha servido para darme cuenta que se pierde el stacktrace en el caso de volver a lanzar otra excepción consecutiva.

De todas maneras tengo dudas sobre si la estructura general que he pensado para controlar el 'burbujeo' 8-D de excepciones es correcta y adecuada, así que pongo este ejemplo:

Primero, es necesario apuntar que una cosa es lo que tiene que informarse al usuario cuando se produce un error (un mensaje general) y otra lo que quiero guardar en el registro de errores...

Supongamos el siguiente caso en el que he pretendido sistematizar el flujo de control de errores y la necesaria pantallita de información para dar al usuario:

He dejado el proyecto de ejemplo en internet, en

sdrv.ms barra YK33rZ

...porque no me cabía en el post.

Según el cual:

-Parece que se hace obligatorio que todas las funciones devuelvan un valor para que el procedimiento que las ha llamando sepa si debe continuar o no.

-Diferentes funciones que nunca se llamaran directamente no mostraran mensajes a usuario, siempre será una superior. Eso sí, cada función a su propio nivel tendrá que guardar ella directamente la información concreta del error (Stacktrace y mensaje, por ejemplo).

-Observar que el throw que se lanza desde la funcion1() cuando se falla la llamada a otras funciones no lleva argumentos porque la función de más arriba sólo le interesa informar de que se ha producido un error en funcion1() en general, no si ha sido al llamar a su vez a otras funciones más profundas, como en este caso la funcion2().

Como siempre, en el stacktrace vemos de verdad la función y línea de orígen de la raíz del problema.

Qué aportarían de más las excepciones personalizadas dentro del esquema que presento, y cómo se podría mejorar ?

Saludos,
Roger