"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:

  1. Con throw en todas partes: Vemos la excepción original desde RepositorioUsuarios con su stack trace completo.
  2. Con throw ex en ServicioUsuarios: Perdemos el origen real en RepositorioUsuarios y parece que el error surgió en ServicioUsuarios.
  3. Con throw new Exception(..., ex) en ControladorUsuarios: 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."

  1. Usa throw por defecto: Preserve el stack trace original siempre que sea posible.
  2. Evita throw ex: Casi nunca hay una buena razón para perder el stack trace original.
  3. Usa throw new Exception(..., ex) con criterio: Cuando necesites añadir contexto significativo sin perder la información original.
  4. 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.