"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:
- Con
throw
en todas partes: Vemos a excepción orixinal desdeRepositorioUsuarios
co seu stack trace completo. - Con
throw ex
enServizoUsuarios
: Perdemos a orixe real enRepositorioUsuarios
e parece que o erro xurdiu enServizoUsuarios
. - Con
throw new Exception(..., ex)
enControladorUsuarios
: 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."
- Usa
throw
por defecto: Preserva o stack trace orixinal sempre que sexa posible. - Evita
throw ex
: Case nunca hai unha boa razón para perder o stack trace orixinal. - Usa
throw new Exception(..., ex)
con criterio: Cando necesites engadir contexto significativo sen perder a información orixinal. -
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.