Práticas recomendadas para captura e repetição de exceções do .NET

284

Quais são as melhores práticas a serem consideradas ao capturar exceções e lançá-las novamente? Quero garantir que o rastreamento do Exceptionobjeto InnerExceptione da pilha sejam preservados. Existe uma diferença entre os seguintes blocos de código na maneira como eles lidam com isso?

try
{
    //some code
}
catch (Exception ex)
{
    throw ex;
}

Vs:

try
{
    //some code
}
catch
{
    throw;
}
Seibar
fonte

Respostas:

262

A maneira de preservar o rastreio da pilha é através do uso de 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; portanto, o rastreamento da pilha iria apenas para onde você está emitindo a throw ex;instrução.

Mike também está correto, supondo que a exceção permita que você passe uma exceção (o que é recomendado).

Karl Seguin tem um ótimo escrever-se na manipulação de exceção em seus Fundamentos de Programação e-book , bem como, o que é uma ótima leitura.

Edit: Link de trabalho para Fundamentos da programação pdf. Basta procurar no texto por "exceção".

Darren Kopp
fonte
10
Não tenho certeza se esse artigo é maravilhoso, sugere tentar {// ...} catch (Exception ex) {lançar nova Exception (ex.Message + "outras coisas"); } é bom. O problema é que você é completamente incapaz de lidar com essa exceção mais longe até a pilha, a menos que você capturar todas as exceções, uma grande falta de nenhum (Tem certeza de que quer lidar com isso OutOfMemoryException?)
LJS
2
@ljs O artigo mudou desde o seu comentário, pois não vejo nenhuma seção em que ele recomenda isso. Muito pelo contrário, ele diz para não fazer isso e pergunta se você também deseja lidar com a OutOfMemoryException !?
RyanfaeScotland
6
Às vezes jogue; não é suficiente para preservar o rastreamento de pilha. Aqui está um exemplo https://dotnetfiddle.net/CkMFoX
Artavazd Balayan
4
Ou ExceptionDispatchInfo.Capture(ex).Throw(); throw;no .NET +4.5 stackoverflow.com/questions/57383/…
Alfred Wallace
A solução @AlfredWallace funcionou perfeitamente para mim. try {...} catch {throw} não preservou o rastreamento de pilha. Obrigado.
atownson
100

Se você lançar uma nova exceção com a exceção inicial, preservará também o rastreio inicial da pilha.

try{
} 
catch(Exception ex){
     throw new MoreDescriptiveException("here is what was happening", ex);
}
Mike
fonte
Não importa o que eu tente lançar nova exceção ("mensagem", ex) sempre lança ex e ignora a mensagem personalizada. lançar nova exceção ("mensagem", ex.InnerException) funciona embora.
Tod
Se nenhuma exceção personalizada for necessária, é possível usar o AggregateException (.NET 4+) msdn.microsoft.com/en-us/library/…
Nikos Tsokos
AggregateExceptiondeve ser usado apenas para exceções sobre operações agregadas. Por exemplo, ele é lançado pelas classes ParallelEnumerablee Taskdo CLR. O uso provavelmente deve seguir este exemplo.
Aluan Haddad 27/02
29

Na verdade, existem algumas situações em que a throwdeclaração não preserva 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 tenha sido gerada 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

Como é um método privado, ele deve ser chamado usando reflexão:

private static void PreserveStackTrace(Exception exception)
{
  MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace",
    BindingFlags.Instance | BindingFlags.NonPublic);
  preserveStackTrace.Invoke(exception, null);
}

Eu tenho uma desvantagem de depender de um método privado para preservar as informações do StackTrace. 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 do blog Fabrice MARGUERIE .

Chamando Exception.SetObjectData

A técnica abaixo foi sugerida por Anton Tykhyy como resposta ao In C #, como posso reconfigurar o InnerException sem perder a pergunta de rastreamento de pilha .

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 confiar apenas em métodos públicos, também depende do construtor de exceções a seguir (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, porque as exceções levantadas por uma biblioteca de terceiros que eu estava usando não implementaram esse construtor.

CARLOS LOTH
fonte
1
Você pode capturar a exceção e publicá-la em qualquer lugar que desejar. Em seguida, jogue um novo explicando o que aconteceu com o usuário. Dessa forma, você pode ver o que aconteceu no momento atual em que a exceção foi capturada, o usuário pode descuidar do que foi a exceção real.
Çöđěxěŕ
2
Com o .NET 4.5, existe uma terceira e - na minha opinião - opção mais limpa: use ExceptionDispatchInfo. Consulte Os tragédios respondem a uma pergunta relacionada aqui: stackoverflow.com/a/17091351/567000 para obter mais informações.
Søren Boisen
20

Quando você throw ex, basicamente, lança uma nova exceção e perde as informações originais de rastreamento da pilha. throwé o método preferido.

Ponto e vírgula esquecido
fonte
13

A regra geral é evitar pegar e jogar o Exceptionobjeto básico . Isso força você a ser um pouco mais inteligente em relação às exceções; em outras palavras, você deve ter uma captura explícita para a SqlExceptionpara que seu código de manipulação não faça algo errado com a NullReferenceException.

No mundo real, porém, capturar e registrar a exceção básica também é uma boa prática, mas não esqueça de acompanhar a coisa toda para obter a que InnerExceptionsela possa ter.

Swilliams
fonte
2
Eu acho que é melhor lidar com exceções sem tratamento para fins de log usando as exceções AppDomain.CurrentDomain.UnhandledException e Application.ThreadException. Usar grandes blocos try {...} catch (Exception ex) {...} em todo lugar significa muita duplicação. Depende se você deseja registrar exceções tratadas; nesse caso, a duplicação (pelo menos mínima) pode ser inevitável.
Ljs
Plus usando esses eventos significa que você fazer log todas as exceções não tratadas, enquanto que se usar grande ol' try {...} catch (Exception ex) {...} blocos que você pode perder alguns.
Ljs 22/06/09
10

Você sempre deve usar "throw"; para rever novamente as exceções no .NET,

Consulte isso, http://weblogs.asp.net/bhouse/archive/2004/11/30/272297.aspx

Basicamente, o MSIL (CIL) tem duas instruções - "throw" e "rehrow":

  • C # "joga ex;" é compilado no "throw" do MSIL
  • "Jogar" do C # - no MSIL "relançar"!

Basicamente, posso ver o motivo pelo qual "throw ex" substitui o rastreamento da pilha.

Vinod T. Patil
fonte
O link - bem, na verdade, a fonte que o link cita - está cheio de boas informações e também observa um possível culpado por que muitos pensam que throw ex;vai relançar - em Java, é verdade! Mas você deve incluir essas informações aqui para obter uma resposta de grau A. (Embora eu ainda esteja alcançando a ExceptionDispatchInfo.Captureresposta de jeuoekdcwzfwccu .)
ruffin
10

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 throw.

A maneira completa de reconfigurar uma exceção capturada é usar ExceptionDispatchInfo.Capture( ex ).Throw()(disponível apenas no .Net 4.5).

Abaixo há 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 em que o número da linha do código-fonte do CallingMethodmétodo é o número da throw new Exception( "TEST" )linha.

No entanto, o caso 3 fornecerá um rastreamento de pilha em que o número da linha do código-fonte do CallingMethodmétodo é o número da linha da throwchamada. Isso significa que, se a throw new Exception( "TEST" )linha estiver cercada por outras operações, você não terá idéia de 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 releitura real, pois altera o tipo da exceção original.

jeuoekdcwzfwccu
fonte
Adicione um resumo simples para nunca usar throw ex;e esta é a melhor resposta de todas.
NH.
8

Algumas pessoas realmente perderam um ponto muito importante - 'throw' e 'throw ex' podem fazer a mesma coisa, mas não fornecem uma peça de informação crucial, que é a linha em que 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', obtém o rastreamento da pilha, mas a linha # será # 22; portanto, não é possível descobrir qual linha estava lançando exatamente a exceção (a menos que você tenha apenas 1 ou poucos linhas de código no bloco try). Para obter a linha 17 esperada em sua exceção, você precisará lançar uma nova exceção com o rastreamento da pilha de exceções original.

notlkk
fonte
3

Você também pode usar:

try
{
// Dangerous code
}
finally
{
// clean up, or do nothing
}

E quaisquer exceções lançadas subirão para o próximo nível que as trata.

Erick B
fonte
3

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.

1kevgriff
fonte
1
Para ser justo em me superar em 2008, o OP estava perguntando como preservar a pilha - e em 2008 eu dei uma resposta correta. O que está faltando na minha resposta é a parte de realmente fazer alguma coisa.
precisa saber é o seguinte
@JohnSaunders Isso é verdade se e somente se você não fizer nada antes throw; por exemplo, você pode limpar um descartável (onde você chama apenas de erro) e depois lançar a exceção.
Meirion Hughes
@meirion quando escrevi o comentário, não havia nada antes do lançamento. Quando isso foi adicionado, eu votei, mas não excluí o comentário.
John Saunders
0

Para sua informação, acabei de testar isso e o rastreamento de pilha relatado por 'throw;' 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 da pilha aponta para a origem da exceção corretamente (número da linha relatada), mas o número da linha relatada para foo () é a linha do lançamento; , portanto, você não pode dizer qual das chamadas para bar () causou a exceção.

redcalx
fonte
Que é por isso que é melhor não tentar capturar exceções a menos que você pretende fazer algo com eles
Nate Zaugg