Estrategias para controlar las excepciones en C#
Al empezar cualquier aplicación es siempre una buena práctica definir una estrategia para controlar los posibles errores que puedan aparecer.
El primer instinto es colocar un try – catch en cada uno de los métodos de la aplicación, pero esto no es necesario, C# tiene mecanismos que permiten centralizar el control de las excepciones.
En este artículo voy a explicar las estrategias principales que uso para controlar el flujo de las excepciones.
Estrategia 1. Guarda la información de la excepción en un Log
Las excepciones ocurren cuando no lo esperamos, por eso cuando suceden es importante capturar toda la información posible y guardarla en un Log (fichero texto, base datos etc.)
En general con tres de las propiedades de la excepción suele haber suficiente información como para saber la causa del error y poder corregirlo.
- MessageError: proporciona una explicación de lo que ha ocurrido.
- InnerException: proporciona información sobre la excepción interna.
- StackTrace: proporciona información de la pila de llamadas antes de la excepción.
La más desconocida de estas propiedades es StackTrace.
StackTrace permite saber la clase, el método e incluso la línea de código donde se produce la excepción. Y no sólo eso, sino que además muestra los métodos anteriores por los que ha pasado el flujo del programa antes de producirse la excepción. Si no guardas esta información es muy probable que no puedas averiguar la causa del error.
Así pues, con esta información se podría escribir un código como este:
try { MetodoQueProvocaUnaExcepcion(); } catch (Exception ex) { // Qué ha sucedido var mensaje = "Error message: " + ex.Message; // Información sobre la excepción interna if (ex.InnerException != null) { mensaje = mensaje + " Inner exception: " + ex.InnerException.Message; } // Dónde ha sucedido mensaje = mensaje + " Stack trace: " + ex.StackTrace; Log(mensaje); }
Estrategia 2. Guarda en el Log Exception.ToString() en lugar Exception.MessageError
Sin embargo, el método ToString() de la excepción ya proporciona toda esta información y el código anterior se puede reducir de la siguiente manera:
try { MetodoQueProvocaUnaExcepcion(); } catch (Exception ex) { Log(ex.ToString()); }
Estrategia 3. Minimiza el número de try - catch
La primera intuición para protegernos contra las excepciones es colocar un try – catch en cada método, pero esto tiene dos claras desventajas:
- El código sería verboso. Cada bloque try – catch se convierte en una interferencia para comprender la intención del código que contiene. Es algo parecido a la publicidad de una web, te acostumbras a ignorarla, pero está ahí, y si desapareciese sentirías un gran alivio
- Rompe con el principio DRY (Don’t Repeat Yourself). Básicamente porque estarías repitiendo el mismo try – catch múltiples veces. Cualquier cambio en la manera de procesar la excepción en el catch se podría convertir en un verdadero tormento.
Se puede lograr exactamente lo mismo colocando un único try – catch en el nivel más alto del código.
Estrategia 4. Colocar un único try – catch en el nivel superior de cada hilo (Thread)
Cuando ocurre una excepción en cualquier punto de la aplicación (o hilo) esta se irá propagando hacia los métodos de las capas superiores hasta llegar al nivel más alto. Por eso no es necesario capturar la excepción en el método exacto que la produce. Si dejamos que se propague y la capturamos en un nivel superior, la propiedad StackTrace ya nos indicará la información sobre el lugar exacto donde se produjo el error.
Estrategia 5. En aplicaciones de cliente (UI) utilizar capturadores de errores globales.
Cuando ocurre un error inesperado en aplicaciones de cliente, si colocamos el try - catch en el nivel superior, por ejemplo en el “main”, el programa terminará después de la excepción. Este comportamiento no es deseable sobre todo porque genera frustración en los usuarios.
En estos casos lo mejor es mostrar un mensaje de error genérico y continuar donde se encontraba el usuario.
Según el tipo de aplicación que desarrolles existen diferentes técnicas para capturar los errores inesperados globlamente:
Winforms
- Application.ThreadException: captura todas las excepciones del hilo principal (Main).
- AppDomain.CurrentDomain.UnhandledException: captura las excepciones provocadas por hilos diferentes al principal.
WPF
- Application.Current.DispatcherUnhandledException: captura las excepciones del hilo principal.
- Dispatcher.UnhandledException: captura las excepciones provocadas por hilos diferentes al principal.
- AppDomain.CurrentDomain.UnhandledException: captura las excepciones de todos los hilos de AppDomain.
- TaskScheduler.UnobservedTaskException: captura las excepciones de los hilos de AppDomain que utiliza un planificador de tareas en operaciones asíncronas.
Asp.Net clásisco
- System.Web.Http.ExceptionHandling.IExceptionLogger: captura cualquier excepción en la aplicación y permite guardarla en un log.
- System.Web.Http.ExceptionHandling.IExceptionHandler: último nivel de captura de excepciones.
- Application_Error (en global.asax): captura cualquier error dentro del hilo que procesa la petición (Request).
Asp.Net Core
- Configurar un HandlerExceptionMiddleware.
Utilizando cualquiera de estas técnicas lograrás centralizar la captura de excepciones en un sólo lugar y conseguirás el mismo efecto que si tuvieras un try - catch en cada uno de los métodos.
Las tecnologías van cambiando, pero el concepto es el mismo. Busca la manera de capturar las excepciones de un modo global.
Estrategia 6. Si trabajas con librerías de terceros captura las excepciones en el nivel más bajo.
Justo al contrario del punto anterior :). Al trabajar con librerías externas a nuestro código, éstas pueden devolver excepciones. Es perfectamente lícito que las librerías externas lancen excepciones porque no conocen el contexto, pero ese contexto en nuestro código puede tener sentido, con lo que podríamos capturar la excepción y procesarla adecuadamente.
Por ejemplo, si queremos escribir en un fichero de texto pero el fichero es de sólo lectura, podríamos capturar la excepción UnauthorizedAccessException con el objetivo de mostrar un mensaje al usuario indicando que el fichero no admite la escritura. Con esa información el usuario puede actuar en consecuencia para conseguir su objetivo.
Esta clase de excepciones deben capturarse en el nivel más bajo:
public void CrearUnaLineaEnFichero(string rutaFichero, string textoLinea) { Fichero fichero = new Fichero(rutaFichero); bool resultado = EscribirLinea(fichero, textoLiena); if (!resultado) { MostrarPorPantalla(“El fichero es de sólo lectura. Selecciona otro fichero”); } } private bool EscribirLinea(Fichero fichero, string textoLinea) { try { using (StreamWriter sw = new StreamWriter(fichero.Ruta)) { file.AppendText(textoLinea); } } catch (UnauthorizedAccessException) { return false; } }
El método únicamente devolverá falso si el fichero es de sólo lectura. Cualquier otra excepción no será capturada dentro del método.
Solo debemos capturar las excepciones que sepamos cómo procesar. En caso de que la librería lance una excepción no conocida lo mejor es dejar que se propague a las capas superiores y que sea capturada por el try – catch global.
Conclusiones:
- Guarda toda la info de las excepciones en un log.
- Exception.ToString() suele ser suficiente para guardar toda la info necesaria.
- Cuantos menos try – catch tenga la aplicación más mantenible será.
- Coloca un try –catch en el nivel superior de cada hilo.
- En aplicaciones cliente busca la manera de capturar las excepciones no controladas de una manera global.
- Para excepciones que sepas procesar de librerías externas coloca el try – catch en la capa más interna.