"Lanzar o no lanzar, esa es la cuestión. Mas si ya tiraste, ¿cómo relanzar sin perder el hilo de la historia?" Hamlet, Acto Exception, Escena Throw
El Trio Mortal: throw, throw ex y throw new Exception
Existen tres formas principales de relanzar excepciones en C#, y cada una tiene consecuencias dramáticas en la forma en que se narra la historia del error:
1. El Espectro Intacto: throw
La sentencia throw
sin argumento es como el fantasma del padre de Hamlet: presenta la verdad completa, sin alteraciones.
try
{
// Código que podría lanzar una excepción
}
catch (Exception ex)
{
// Registrar el error o hacer algún manejo
_logger.Error($"Ocurrió un error: {ex.Message}");
throw; // Relanza conservando el stack trace original
}
Este método preserva el stack trace original, manteniendo intacta toda la información sobre dónde se originó la excepción. Es como si el mensajero relatara fielmente los eventos sin alterar ni una sola palabra.
2. El Usurpador del Trono: throw ex
La sentencia throw ex
es como Claudio, el tío de Hamlet: borra la historia y comienza a narrar desde su punto de vista.
try
{
// Código que podría lanzar una excepción
}
catch (Exception ex)
{
// Registrar el error o hacer algún manejo
_logger.Error($"Ocurrió un error: {ex.Message}");
throw ex; // ¡Problema! Reinicia el stack trace
}
Este enfoque reinicia el stack trace, haciendo que comience desde el punto donde se llama a throw ex
. Como resultado, perdemos todo el contexto original donde ocurrió la excepción. Es como si Claudio reescribiera la historia de la muerte del rey para ocultar su participación.
Veamos cómo afecta esto a nuestro stack trace:
// Stack trace con `throw`
System.IO.FileNotFoundException: No se pudo encontrar el archivo 'hamlet.txt'.
en System.IO.File.ReadAllText(...) en línea 123
en MiAplicacion.LectorDeArchivos.Leer(...) en línea 45
en MiAplicacion.Program.Main(...) en línea 7
// Stack trace con `throw ex`
System.IO.FileNotFoundException: No se pudo encontrar el archivo 'hamlet.txt'.
en MiAplicacion.LectorDeArchivos.Leer(...) en línea 45 // ¡Aquí comienza!
en MiAplicacion.Program.Main(...) en línea 7
¡Perdimos la información sobre el origen real de la excepción! Ya no sabemos que fue System.IO.File.ReadAllText
quien originalmente la lanzó.
3. El Narrador Que Enriquece: throw new Exception(mensaje, ex)
Esta tercera forma es como Horacio, el amigo de Hamlet: cuenta la historia original pero añade su propia interpretación y contexto.
try
{
// Código que podría lanzar una excepción
}
catch (Exception ex)
{
// Registrar el error o hacer algún manejo
_logger.Error($"Ocurrió un error: {ex.Message}");
throw new Exception("Error al procesar el documento", ex);
}
Con este enfoque, creamos una nueva excepción con un mensaje personalizado, y pasamos la excepción original como excepción interna. Esto proporciona contexto adicional mientras preserva los detalles de la excepción original. Es como si Horacio añadiera su interpretación a los eventos mientras mantiene la historia original intacta.
Los Soliloquios de Nuestros Errores: Diferencias Prácticas
Preservación del Stack Trace
throw
: Como el fantasma honesto, preserva el stack trace original, proporcionando información completa sobre dónde se originó la excepción.throw ex
: Como el usurpador, reinicia el stack trace, dificultando la depuración de la causa raíz de la excepción.throw new Exception("Mensaje de error", ex)
: Como el narrador sabio, proporciona contexto adicional con un nuevo mensaje de excepción mientras preserva la excepción original como excepción interna.
Casos de Uso
throw
: Úsalo cuando quieras mantener el contexto original de la excepción y el stack trace, que es casi siempre el enfoque preferido.throw ex
: Úsalo con moderación, y solo cuando necesites ocultar intencionalmente detalles de implementación internos (algo que raramente querrás hacer).throw new Exception("Mensaje de error", ex)
: Úsalo cuando quieras añadir más contexto a la excepción mientras mantienes los detalles de la excepción original.
La Tragedia de Perder el Stack Trace
"¡Ay, pobre Yorick! El stack trace conocía rutas infinitas y ahora, ¡borrado está!"
Veamos un ejemplo concreto para entender el impacto de estas diferencias. Imagina una aplicación de varios niveles donde un método lanza una excepción. La forma en que se relanza la excepción puede afectar significativamente el proceso de depuración.
Considera este código:
// Capa de acceso a datos
public class RepositorioUsuarios
{
public Usuario ObtenerPorId(string id)
{
try
{
// Simula una operación de base de datos
if (string.IsNullOrEmpty(id))
throw new ArgumentException("El ID no puede estar vacío");
// Más código...
return null; // Usuario no encontrado
}
catch (Exception ex)
{
_logger.Error($"Error en RepositorioUsuarios: {ex.Message}");
throw; // Relanza preservando el stack trace
}
}
}
// Capa de servicios
public class ServicioUsuarios
{
private readonly RepositorioUsuarios _repo;
public Usuario ObtenerPorId(string id)
{
try
{
return _repo.ObtenerPorId(id);
}
catch (Exception ex)
{
_logger.Error($"Error en ServicioUsuarios: {ex.Message}");
throw ex; // ¡MAL! Reinicia el stack trace
}
}
}
// Capa de API
public class ControladorUsuarios
{
private readonly ServicioUsuarios _servicio;
public Usuario ObtenerPorId(string id)
{
try
{
return _servicio.ObtenerPorId(id);
}
catch (Exception ex)
{
_logger.Error($"Error en API: {ex.Message}");
throw new Exception($"Error al obtener usuario {id}", ex); // Enriquece con contexto
}
}
}
Ahora, cuando depuramos, vemos estos diferentes escenarios:
- Con
throw
en todas partes: Vemos la excepción original desdeRepositorioUsuarios
con su stack trace completo. - Con
throw ex
enServicioUsuarios
: Perdemos el origen real enRepositorioUsuarios
y parece que el error surgió enServicioUsuarios
. - Con
throw new Exception(..., ex)
enControladorUsuarios
: Vemos un mensaje enriquecido pero aún podemos acceder a la excepción original y su información.
Midiendo el Coste del Drama
El coste en rendimiento de relanzar excepciones no es tan dramático como el de lanzarlas inicialmente (que vimos es hasta 925% más lento). Sin embargo, hay pequeñas diferencias:
throw
: El más eficiente, ya que simplemente propaga la excepción existente.throw ex
: Ligeramente más costoso, ya que reconstruye el stack trace.throw new Exception(..., ex)
: El más costoso de los tres, ya que crea un nuevo objeto de excepción además de mantener la referencia a la excepción original.
Pero honestamente, si ya estás lanzando una excepción, la diferencia de rendimiento entre estos tres métodos es insignificante comparada con el coste inicial de lanzar la excepción.
El Epílogo Sabio: Mejores Prácticas
"Que vuestras excepciones cuenten la historia completa, pues solo así podrán los depuradores encontrar la verdad."
- Usa
throw
por defecto: Preserve el stack trace original siempre que sea posible. - Evita
throw ex
: Casi nunca hay una buena razón para perder el stack trace original. - Usa
throw new Exception(..., ex)
con criterio: Cuando necesites añadir contexto significativo sin perder la información original. -
Considera excepciones personalizadas: Para errores específicos de dominio, crear tus propias clases de excepción puede ser más claro que enriquecer excepciones genéricas.
public class UsuarioNoEncontradoException : Exception {
public UsuarioNoEncontradoException(string usuarioId) : base($"Usuario con ID '{usuarioId}' no encontrado") {
}public UsuarioNoEncontradoException(string usuarioId, Exception innerException) : base($"Usuario con ID '{usuarioId}' no encontrado", innerException) { }
}
Conclusión
Como Hamlet con su dilema existencial, debemos elegir sabiamente cómo relanzar nuestras excepciones:
throw
: El fantasma honesto que mantiene la verdad intacta.throw ex
: El usurpador que oculta la historia original.throw new Exception(..., ex)
: El narrador que enriquece la historia sin perder su esencia.
Elige el throw
simple en la mayoría de los casos, y dormirás mejor por las noches, libre de los fantasmas de los errores sin contexto y los stack traces incompletos.
Esta sección es parte del artículo Lanzar o no lanzar una excepción? Esa es la cuestión. Sigue atento para el tercer y último acto donde exploraremos el patrón Result
como alternativa elegante al lanzamiento de excepciones.