Ir al contenido principal

Buenas prácticas en el manejo de excepciones

El manejo de excepciones no es algo trivial. Es difícil de entender para los principiantes, y desarrolladores más experimentados suelen tener opiniones diferentes sobre que excepciones deben ser lanzadas o manejadas.
Muchos equipos de desarrollo tienen un conjunto de reglas propias sobre como manejar las excepciones. Sin embargo, hay buenas prácticas que son utilizadas por la mayoría de los equipos.

No se debe capturar una excepcion que no se procese

Una tentación enorme que existe es la de hacer caso omiso de las excepciones no planeadas que se produzcan. Es decir, si se produce una excepción con la que no contábamos, simplemente la capturamos para que no rompa la aplicación. El código sería algo como esto:
try {
   // Código de una función o método
} catch (Exception ex) {
   ...
}
Si no se sabe que hacer con una excepción, se debe dejar que flote hacia arriba hasta que alguien sepa que hacer con ella. Al no saber que hacer con la excepción es demasiado fácil caer en la tentación de gestionarla de manera superficial, y seguramente equivocada:
  • Ignorando el error sin hacer nada, impidiendo su futura localización.
  • Mostrando toda la traza del error sin filtrar, y comprometiendo datos sensibles.
  • Logeando la excepción y relanzando, lo que puede llenar el log de trazas repetidas.

No se debe volcar la excepción sin más.

Revelar demasiada información con una excepción es una práctica común en muchos desarrolladores.
try {
   // Código de una función o método
} catch (Exception ex) {
   ex.printStackTrace();
}
En este caso se muestra por la consola el error al completo, pero también existen otras variedades de está practica: volcar la excepción al completo en el buffer de respuesta de una petición REST también tiene el mismo efecto.
El problema con esta técnica es que al final acaba llegando al usuario final toda la información de la excepción. Existe mucha información sensible que se mostrará en el texto de la excepción que no debería ser visible para los usuarios:
  • Si tenemos un error de conexión con una bbdd, la excepción mostrará la url, el usuario y la contraseña utilizados. El error de conexión puede ser causado por un fallo temporal del servidor de base de datos, y las credenciales que se muestran en la traza pueden ser válidas
  • Si tenemos un error al intentar leer o escribir un fichero, se mostrará la ruta completa de ficheros en donde se está trabajando. En una aplicación de servidor, eso arroja información delicada que puede ser aprovechada para conocer detalles de configuración del servidor.
  • Si hay un error en una consulta SQL, la traza mostará la sql completa; que tendrá nombres de tablas y columnas.

No se debe silenciar una excepción

Este caso es justo el opuesto al anterior, pero es también extremadamente común.
try {
   // Código de una función o método
} catch (Exception ex) {
}
El código evoluciona, y puede cambiar en el futuro. Alguien podría eliminar la validación que impidió el evento excepcional sin reconocer que esto crea un problema. O el código que arroja la excepción se cambia y ahora arroja múltiples excepciones de la misma clase, y el código de llamada no los previene a todos.
Cuando se produzca una error que no tenías contemplado en tu código ni siquiera te enterarás. Algo fallará pero no quedará rastro de ellos. Por eso te resultará casi imposible averiguar qué está pasando, y mucho menos aún depurar la aplicación.

No registre una excepción para lanzarla de nuevo

Se trata de un error bastante habitual:
try {
   // Código de una función o método
} catch (Exception ex) {
   logger.error( ex );
   throw ex;
}
Puede ser intuitivo registrar una excepción cuando ocurrió y luego volver a lanzarla para que la persona que la llame pueda manejarla adecuadamente. Pero el resultado final será que se estarán registrando múltiples mensajes de error para la misma excepción. El problema es que cuando revisemos el log para ver lo que pueda estar pasando, tendremos una gran cantidad de mensajes repetidos para el mismo error, lo que va a dificultar la comprensión del flujo.

No relancemos las excepciones sin más

Cuando se trabaja con un debugger paso a paso, pueden crearse bloque try catch para observar la ejecución paso a paso; pero al final queda un bloque de código que no hace nada:
try {
   // Código de una función o método
} catch (Exception ex) {
   throw ex;
}
Se está introduciendo un bloque de código de manejo de excepciones que es costoso computacionalmente, para no hacer nada con él. El efecto sería exactamente el mismo que quitar el bloque try - catch.

No eliminar la causa original de la excepción

Una práctica habitual y muy correcta es la de definir excepciones propias basadas en el modelo de negocio. Cuando se produce una condicion excepcional, en lugar de dejar el error genérico se genera una excepción de negocio que tenga significado (por ejemplo en lugar de dejar que salte una excepción de "NumberFormatException" al leer el la configuración de timeout, se puede usar una excepción de ConfigurationException).
El problema aqui surge cuando se elimina la excepción original. Es lo que ocurre en el siguiente código:
try {
   // Código de una función o método
} catch (NumberFormatException ex) {
   throw new MyBusinessException(ErrorCode.CONFIGURATION_ERROR);
}
Lo correcto es envolver la excepción original con la excepción de negocio. De lo contrario, perderá el mensaje y el seguimiento de la pila que describe el evento excepcional que causó su excepción.
try {
   // Código de una función o método
} catch (NumberFormatException ex) {
   throw new MyBusinessException(ex, ErrorCode.CONFIGURATION_ERROR);
}

Comentarios