Práticas recomendadas para capturar e relançar exceções do .NET
-
09-06-2019 - |
Pergunta
Quais são as melhores práticas a serem consideradas ao capturar exceções e relançá-las?Quero ter certeza de que Exception
objeto InnerException
e o rastreamento de pilha são preservados.Existe alguma diferença entre os seguintes blocos de código na maneira como eles lidam com isso?
try
{
//some code
}
catch (Exception ex)
{
throw ex;
}
Contra:
try
{
//some code
}
catch
{
throw;
}
Solução
A maneira de preservar o rastreamento de pilha é através do uso do throw;
Isso também é válido
try {
// something that bombs here
} catch (Exception ex)
{
throw;
}
throw ex;
é basicamente como lançar uma exceção a partir desse ponto, então o rastreamento de pilha só iria para onde você está emitindo o throw ex;
declaração.
Mike também está correto, assumindo que a exceção permite passar uma exceção (o que é recomendado).
Karl Seguin tem um ótimo artigo sobre tratamento de exceções No dele e-book fundamentos da programação também, o que é uma ótima leitura.
Editar:Link de trabalho para Fundamentos da Programação pdf.Basta pesquisar no texto por "exceção".
Outras dicas
Se você lançar uma nova exceção com a exceção inicial, você também preservará o rastreamento de pilha inicial.
try{
}
catch(Exception ex){
throw new MoreDescriptiveException("here is what was happening", ex);
}
Na verdade, existem algumas situações em que o throw
declaração não preservará as informações do StackTrace.Por exemplo, no código abaixo:
try
{
int i = 0;
int j = 12 / i; // Line 47
int k = j + 1;
}
catch
{
// do something
// ...
throw; // Line 54
}
O StackTrace indicará que a linha 54 gerou a exceção, embora ela tenha sido levantada na linha 47.
Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
at Program.WithThrowIncomplete() in Program.cs:line 54
at Program.Main(String[] args) in Program.cs:line 106
Em situações como a descrita acima, existem duas opções para preservar o StackTrace original:
Chamando o Exception.InternalPreserveStackTrace
Por ser um método privado, deve ser invocado usando reflexão:
private static void PreserveStackTrace(Exception exception)
{
MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace",
BindingFlags.Instance | BindingFlags.NonPublic);
preserveStackTrace.Invoke(exception, null);
}
Tenho a desvantagem de depender de um método privado para preservar as informações do StackTrace.Ele pode ser alterado em versões futuras do .NET Framework.O exemplo de código acima e a solução proposta abaixo foram extraídos de Blog de Fabrice MARGUERIE.
Chamando Exception.SetObjectData
A técnica abaixo foi sugerida por Anton Tykhyy como resposta a Em C#, como posso relançar InnerException sem perder o rastreamento de pilha pergunta.
static void PreserveStackTrace (Exception e)
{
var ctx = new StreamingContext (StreamingContextStates.CrossAppDomain) ;
var mgr = new ObjectManager (null, ctx) ;
var si = new SerializationInfo (e.GetType (), new FormatterConverter ()) ;
e.GetObjectData (si, ctx) ;
mgr.RegisterObject (e, 1, si) ; // prepare for SetObjectData
mgr.DoFixups () ; // ObjectManager calls SetObjectData
// voila, e is unmodified save for _remoteStackTraceString
}
Embora tenha a vantagem de depender apenas de métodos públicos, também depende do seguinte construtor de exceção (que algumas exceções desenvolvidas por terceiros não implementam):
protected Exception(
SerializationInfo info,
StreamingContext context
)
Na minha situação, tive que escolher a primeira abordagem, pois as exceções levantadas por uma biblioteca de terceiros que eu estava usando não implementavam esse construtor.
Quando você throw ex
, você estará basicamente lançando uma nova exceção e perderá as informações originais de rastreamento de pilha. throw
é o método preferido.
A regra é evitar pegar e jogar o básico Exception
objeto.Isso força você a ser um pouco mais esperto em relação às exceções;em outras palavras, você deve ter uma captura explícita para um SqlException
para que seu código de manipulação não faça algo errado com um NullReferenceException
.
No mundo real, porém, pegar e registro a exceção base também é uma boa prática, mas não se esqueça de percorrer tudo para obter qualquer InnerExceptions
deve haver.
Ninguém explicou a diferença entre ExceptionDispatchInfo.Capture( ex ).Throw()
e uma planície throw
, Então aqui está.No entanto, algumas pessoas notaram o problema com throw
.
A maneira completa de relançar uma exceção capturada é usar ExceptionDispatchInfo.Capture( ex ).Throw()
(disponível apenas em .Net 4.5).
Abaixo estão os casos necessários para testar isso:
1.
void CallingMethod()
{
//try
{
throw new Exception( "TEST" );
}
//catch
{
// throw;
}
}
2.
void CallingMethod()
{
try
{
throw new Exception( "TEST" );
}
catch( Exception ex )
{
ExceptionDispatchInfo.Capture( ex ).Throw();
throw; // So the compiler doesn't complain about methods which don't either return or throw.
}
}
3.
void CallingMethod()
{
try
{
throw new Exception( "TEST" );
}
catch
{
throw;
}
}
4.
void CallingMethod()
{
try
{
throw new Exception( "TEST" );
}
catch( Exception ex )
{
throw new Exception( "RETHROW", ex );
}
}
O caso 1 e o caso 2 fornecerão um rastreamento de pilha onde o número da linha do código-fonte do CallingMethod
método é o número da linha do throw new Exception( "TEST" )
linha.
No entanto, o caso 3 fornecerá um rastreamento de pilha onde o número da linha do código-fonte do CallingMethod
método é o número da linha do throw
chamar.Isto significa que se o throw new Exception( "TEST" )
linha estiver cercada por outras operações, você não terá ideia de em qual número de linha a exceção foi realmente lançada.
O caso 4 é semelhante ao caso 2 porque o número da linha da exceção original é preservado, mas não é uma repetição real porque altera o tipo da exceção original.
Algumas pessoas realmente perderam um ponto muito importante - 'throw' e 'throw ex' podem fazer a mesma coisa, mas não fornecem uma informação crucial que é a linha onde a exceção aconteceu.
Considere o seguinte código:
static void Main(string[] args)
{
try
{
TestMe();
}
catch (Exception ex)
{
string ss = ex.ToString();
}
}
static void TestMe()
{
try
{
//here's some code that will generate an exception - line #17
}
catch (Exception ex)
{
//throw new ApplicationException(ex.ToString());
throw ex; // line# 22
}
}
Quando você faz um 'throw' ou 'throw ex' você obtém o rastreamento de pilha, mas a linha # será a # 22, então você não consegue descobrir qual linha exatamente estava lançando a exceção (a menos que você tenha apenas 1 ou poucos linhas de código no bloco try).Para obter a linha esperada nº 17 em sua exceção, você terá que lançar uma nova exceção com o rastreamento de pilha de exceção original.
Você sempre deve usar "arremesso"; para repensar as exceções em .net,
Consulte isto,http://weblogs.asp.net/bhouse/archive/2004/11/30/272297.aspx
Basicamente, o MSIL (CIL) possui duas instruções - "lançar" e "relançar":
- C#"Trow Ex;" é compilado em "Tiro" de Msil
- C#'s "arremesso;" - No MSIL "Rethrow"!
Basicamente, posso ver o motivo pelo qual "throw ex" substitui o rastreamento de pilha.
Você também pode usar:
try
{
// Dangerous code
}
finally
{
// clean up, or do nothing
}
E quaisquer exceções lançadas irão passar para o próximo nível que as trata.
Eu definitivamente usaria:
try
{
//some code
}
catch
{
//you should totally do something here, but feel free to rethrow
//if you need to send the exception up the stack.
throw;
}
Isso preservará sua pilha.
Para sua informação, acabei de testar isso e o rastreamento da pilha relatado por 'arremesso;' não é um rastreamento de pilha totalmente correto.Exemplo:
private void foo()
{
try
{
bar(3);
bar(2);
bar(1);
bar(0);
}
catch(DivideByZeroException)
{
//log message and rethrow...
throw;
}
}
private void bar(int b)
{
int a = 1;
int c = a/b; // Generate divide by zero exception.
}
O rastreamento de pilha aponta corretamente para a origem da exceção (número da linha relatado), mas o número da linha relatado para foo() é a linha do lançamento;instrução, portanto você não pode dizer qual das chamadas para bar() causou a exceção.