"Lanzar ou non lanzar, esa é a cuestión. Mais se xa tiraches, como relanzar sen perder o fío da historia?" Hamlet, Acto Exception, Escena Throw

O Trío Mortal: throw, throw ex e throw new Exception

Existen tres formas principais de relanzar excepcións en C#, e cada unha ten consecuencias dramáticas na forma en que se narra a historia do erro:

1. O Espectro Intacto: throw

A sentenza throw sen argumento é como o fantasma do pai de Hamlet: presenta a verdade completa, sen alteracións.

try
{
    // Código que podería lanzar unha excepción
}
catch (Exception ex)
{
    // Rexistrar o erro ou facer algún manexo
    _logger.Error($"Ocorreu un erro: {ex.Message}");
    throw; // Relanza conservando o stack trace orixinal
}

Este método preserva o stack trace orixinal, mantendo intacta toda a información sobre onde se orixinou a excepción. É como se o mensaxeiro relatase fielmente os eventos sen alterar nin unha soa palabra.

2. O Usurpador do Trono: throw ex

A sentenza throw ex é como Claudio, o tío de Hamlet: borra a historia e comeza a narrar desde o seu punto de vista.

try
{
    // Código que podería lanzar unha excepción
}
catch (Exception ex)
{
    // Rexistrar o erro ou facer algún manexo
    _logger.Error($"Ocorreu un erro: {ex.Message}");
    throw ex; // Problema! Reinicia o stack trace
}

Este enfoque reinicia o stack trace, facendo que comece desde o punto onde se chama a throw ex. Como resultado, perdemos todo o contexto orixinal onde ocorreu a excepción. É como se Claudio reescribise a historia da morte do rei para ocultar a súa participación.

Vexamos como afecta isto ao noso stack trace:

// Stack trace con `throw`
System.IO.FileNotFoundException: Non se puido atopar o ficheiro 'hamlet.txt'.
   en System.IO.File.ReadAllText(...) en liña 123
   en MiñaAplicacion.LectorDeFicheiros.Ler(...) en liña 45
   en MiñaAplicacion.Program.Main(...) en liña 7

// Stack trace con `throw ex`
System.IO.FileNotFoundException: Non se puido atopar o ficheiro 'hamlet.txt'.
   en MiñaAplicacion.LectorDeFicheiros.Ler(...) en liña 45 // Aquí comeza!
   en MiñaAplicacion.Program.Main(...) en liña 7

Perdemos a información sobre a orixe real da excepción! Xa non sabemos que foi System.IO.File.ReadAllText quen orixinalmente a lanzou.

3. O Narrador Que Enriquece: throw new Exception(mensaxe, ex)

Esta terceira forma é como Horacio, o amigo de Hamlet: conta a historia orixinal pero engade a súa propia interpretación e contexto.

try
{
    // Código que podería lanzar unha excepción
}
catch (Exception ex)
{
    // Rexistrar o erro ou facer algún manexo
    _logger.Error($"Ocorreu un erro: {ex.Message}");
    throw new Exception("Erro ao procesar o documento", ex);
}

Con este enfoque, creamos unha nova excepción cunha mensaxe personalizada, e pasamos a excepción orixinal como excepción interna. Isto proporciona contexto adicional mentres preserva os detalles da excepción orixinal. É como se Horacio engadise a súa interpretación aos eventos mentres mantén a historia orixinal intacta.

Os Soliloquios dos Nosos Erros: Diferenzas Prácticas

Preservación do Stack Trace

  • throw: Como o fantasma honesto, preserva o stack trace orixinal, proporcionando información completa sobre onde se orixinou a excepción.
  • throw ex: Como o usurpador, reinicia o stack trace, dificultando a depuración da causa raíz da excepción.
  • throw new Exception("Mensaxe de erro", ex): Como o narrador sabio, proporciona contexto adicional cunha nova mensaxe de excepción mentres preserva a excepción orixinal como excepción interna.

Casos de Uso

  • throw: Úsao cando queiras manter o contexto orixinal da excepción e o stack trace, que é case sempre o enfoque preferido.
  • throw ex: Úsao con moderación, e só cando necesites ocultar intencionalmente detalles de implementación internos (algo que raramente quererás facer).
  • throw new Exception("Mensaxe de erro", ex): Úsao cando queiras engadir máis contexto á excepción mentres mantés os detalles da excepción orixinal.

A Traxedia de Perder o Stack Trace

"Ai, pobre Yorick! O stack trace coñecía rutas infinitas e agora, borrado está!"

Vexamos un exemplo concreto para entender o impacto destas diferenzas. Imaxina unha aplicación de varios niveis onde un método lanza unha excepción. A forma en que se relanza a excepción pode afectar significativamente o proceso de depuración.

Considera este código:

// Capa de acceso a datos
public class RepositorioUsuarios
{
    public Usuario ObterPorId(string id)
    {
        try
        {
            // Simula unha operación de base de datos
            if (string.IsNullOrEmpty(id))
                throw new ArgumentException("O ID non pode estar baleiro");

            // Máis código...
            return null; // Usuario non atopado
        }
        catch (Exception ex)
        {
            _logger.Error($"Erro en RepositorioUsuarios: {ex.Message}");
            throw; // Relanza preservando o stack trace
        }
    }
}

// Capa de servizos
public class ServizoUsuarios
{
    private readonly RepositorioUsuarios _repo;

    public Usuario ObterPorId(string id)
    {
        try
        {
            return _repo.ObterPorId(id);
        }
        catch (Exception ex)
        {
            _logger.Error($"Erro en ServizoUsuarios: {ex.Message}");
            throw ex; // MAL! Reinicia o stack trace
        }
    }
}

// Capa de API
public class ControladorUsuarios
{
    private readonly ServizoUsuarios _servizo;

    public Usuario ObterPorId(string id)
    {
        try
        {
            return _servizo.ObterPorId(id);
        }
        catch (Exception ex)
        {
            _logger.Error($"Erro na API: {ex.Message}");
            throw new Exception($"Erro ao obter usuario {id}", ex); // Enriquece con contexto
        }
    }
}

Agora, cando depuramos, vemos estes diferentes escenarios:

  1. Con throw en todas partes: Vemos a excepción orixinal desde RepositorioUsuarios co seu stack trace completo.
  2. Con throw ex en ServizoUsuarios: Perdemos a orixe real en RepositorioUsuarios e parece que o erro xurdiu en ServizoUsuarios.
  3. Con throw new Exception(..., ex) en ControladorUsuarios: Vemos unha mensaxe enriquecida pero aínda podemos acceder á excepción orixinal e á súa información.

Medindo o Custo do Drama

O custo en rendemento de relanzar excepcións non é tan dramático como o de lanzalas inicialmente (que vimos é ata 925% máis lento). Non obstante, hai pequenas diferenzas:

  • throw: O máis eficiente, xa que simplemente propaga a excepción existente.
  • throw ex: Lixeiramente máis custoso, xa que reconstrúe o stack trace.
  • throw new Exception(..., ex): O máis custoso dos tres, xa que crea un novo obxecto de excepción ademais de manter a referencia á excepción orixinal.

Pero honestamente, se xa estás lanzando unha excepción, a diferenza de rendemento entre estes tres métodos é insignificante comparada co custo inicial de lanzar a excepción.

O Epílogo Sabio: Mellores Prácticas

"Que as vosas excepcións conten a historia completa, pois só así poderán os depuradores atopar a verdade."

  1. Usa throw por defecto: Preserva o stack trace orixinal sempre que sexa posible.
  2. Evita throw ex: Case nunca hai unha boa razón para perder o stack trace orixinal.
  3. Usa throw new Exception(..., ex) con criterio: Cando necesites engadir contexto significativo sen perder a información orixinal.
  4. Considera excepcións personalizadas: Para erros específicos de dominio, crear as túas propias clases de excepción pode ser máis claro que enriquecer excepcións xenéricas.

    public class UsuarioNonAtopadoException : Exception {
    public UsuarioNonAtopadoException(string usuarioId) : base($"Usuario con ID '{usuarioId}' non atopado") {
    }

    public UsuarioNonAtopadoException(string usuarioId, Exception innerException)
        : base($"Usuario con ID '{usuarioId}' non atopado", innerException)
    {       
    }
    

    }

Conclusión

Como Hamlet co seu dilema existencial, debemos elixir sabiamente como relanzar as nosas excepcións:

  • throw: O fantasma honesto que mantén a verdade intacta.
  • throw ex: O usurpador que oculta a historia orixinal.
  • throw new Exception(..., ex): O narrador que enriquece a historia sen perder a súa esencia.

Elixe o throw simple na maioría dos casos, e durmirás mellor polas noites, libre dos fantasmas dos erros sen contexto e os stack traces incompletos.


Esta sección é parte do artigo Lanzar ou non lanzar unha excepción? Esa é a cuestión. Mantente atento para o terceiro e último acto onde exploraremos o patrón Result como alternativa elegante ao lanzamento de excepcións.