Por que capturar e repetir uma exceção em C #?

557

Eu estou vendo o artigo C # - Data Transfer Object em DTOs serializáveis.

O artigo inclui este pedaço de código:

public static string SerializeDTO(DTO dto) {
    try {
        XmlSerializer xmlSer = new XmlSerializer(dto.GetType());
        StringWriter sWriter = new StringWriter();
        xmlSer.Serialize(sWriter, dto);
        return sWriter.ToString();
    }
    catch(Exception ex) {
        throw ex;
    }
}

O restante do artigo parece sensato e razoável (para um noob), mas esse try-catch-throw lança uma WtfException ... Isso não é exatamente equivalente a não lidar com exceções?

Ergo:

public static string SerializeDTO(DTO dto) {
    XmlSerializer xmlSer = new XmlSerializer(dto.GetType());
    StringWriter sWriter = new StringWriter();
    xmlSer.Serialize(sWriter, dto);
    return sWriter.ToString();
}

Ou estou perdendo algo fundamental sobre o tratamento de erros em c #? É praticamente o mesmo que Java (menos exceções verificadas), não é? ... Ou seja, ambos refinaram o C ++.

A questão do estouro de pilha A diferença entre relançar a captura sem parâmetros e não fazer nada? parece apoiar minha afirmação de que o try-catch-throw é um não-op.


EDITAR:

Apenas para resumir para quem encontrar este tópico no futuro ...

NÃO

try {
    // Do stuff that might throw an exception
}
catch (Exception e) {
    throw e; // This destroys the strack trace information!
}

As informações de rastreamento da pilha podem ser cruciais para identificar a causa raiz do problema!

FAZ

try {
    // Do stuff that might throw an exception
}
catch (SqlException e) {
    // Log it
    if (e.ErrorCode != NO_ROW_ERROR) { // filter out NoDataFound.
        // Do special cleanup, like maybe closing the "dirty" database connection.
        throw; // This preserves the stack trace
    }
}
catch (IOException e) {
    // Log it
    throw;
}
catch (Exception e) {
    // Log it
    throw new DAOException("Excrement occurred", e); // wrapped & chained exceptions (just like java).
}
finally {
    // Normal clean goes here (like closing open files).
}

Capte as exceções mais específicas antes das menos específicas (assim como Java).


Referências:

Corlettk
fonte
8
Bom resumo; pontos extras por incluir o bloco final.
2111 Fredrik Mörk
eu gostaria de acrescentar que você pode usar o "throw;" para ser ainda mais útil adicionando os parâmetros que foram enviados ao método na coleção e.Data antes do "throw"; declaração
Michael Bahig 28/04
@MickTheWarMachineDesigner (e pintor em meio período). Hã? Você está falando sobre as exceções da Microshite Suckwell (provavelmente de 2005 em diante, pelo que sei). Eu estava falando sobre tratamento de exceções em geral. E sim, eu aprendi algumas desde que publiquei isso QUASE QUATRO ANOS ATRÁS ... Mas sim, confesso que você tem um ponto válido, mas eu acho que você perdeu o ponto real; se é que me entende? Esta pergunta é sobre manipulação de exceção GENERALIZED em C #; e mais especificamente sobre relançar exceções ... de TODOS os tipos. Legal?
corlettk
Considere mover a seção de resumo da edição da sua pergunta para sua própria resposta. Por que, consulte Edição da resposta automática fora de questão e Resposta incorporada na pergunta .
DavidRR
2
Alguém não notou a parte "Excremento ocorreu"? parece que o código foi para um cocô!
Jason Loki Smith

Respostas:

430

Primeiro; a maneira como o código no artigo faz isso é ruim. throw exredefinirá a pilha de chamadas na exceção para o ponto em que esta instrução throw está; perdendo as informações sobre onde a exceção realmente foi criada.

Segundo, se você capturar e jogar novamente dessa maneira, não vejo valor agregado, o exemplo de código acima seria tão bom (ou, throw exmelhor ainda) sem o try-catch.

No entanto, há casos em que você pode querer capturar e relançar uma exceção. O log pode ser um deles:

try 
{
    // code that may throw exceptions    
}
catch(Exception ex) 
{
    // add error logging here
    throw;
}
Fredrik Mörk
fonte
6
@ Fredrick, apenas fyi (embora você provavelmente saiba) se não for usar esse exobjeto, não será necessário instanciar o objeto.
Eoin Campbell
78
@Eoin: Se não for instanciado, seria bastante difícil registrá-lo.
21339 Sam Ax
30
Sim, acho que "mal" está certo ... considere o caso da exceção de ponteiro nulo lançada em algum lugar de um grande corpo de código. A mensagem é baunilha, sem o rastreamento da pilha, você fica com "algo estava nulo em algum lugar". NÃO é bom quando a produção está morta; e você NÃO tem minutos ou menos para resolver o problema do flamin e descartá-lo ou corrigi-lo ... Um bom manuseio de exceções vale o peso em ouro.
Corlettk 19/05/09
4
Isso é verdade para Java também ... "throw" vs. "throw ex"?
JasonStoltz
8
@ Jason, veja esta pergunta . Em Java, throw exnão reinicia o stacktrace.
Matthew Flaschen
117

Não faça isso,

try 
{
...
}
catch(Exception ex)
{
   throw ex;
}

Você perderá as informações de rastreamento da pilha ...

Qualquer um,

try { ... }
catch { throw; }

OU

try { ... }
catch (Exception ex)
{
    throw new Exception("My Custom Error Message", ex);
}

Uma das razões pelas quais você pode querer relançar novamente é se estiver lidando com exceções diferentes, por exemplo,

try
{
   ...
}
catch(SQLException sex)
{
   //Do Custom Logging 
   //Don't throw exception - swallow it here
}
catch(OtherException oex)
{
   //Do something else
   throw new WrappedException("Other Exception occured");
}
catch
{
   System.Diagnostics.Debug.WriteLine("Eeep! an error, not to worry, will be handled higher up the call stack");
   throw; //Chuck everything else back up the stack
}
Eoin Campbell
fonte
7
Por que não deixar completamente de lado o apanha?
21139 AnthonyWJones
3
ainda há valor em sair do catch {throw; } no final de uma lista de capturas de tipos de exceção específicos, com base no fato de provar que o autor considerou o caso, embora um comentário possa ser suficiente. Não adivinhar quando você lê código é uma coisa boa.
Annakata 19/05/09
87
Por alguma razão, o nome da SQLException me incomoda.
Michael Myers
13
Esse problema (Exception) {throw new Exception (...)} é algo que você nunca deve fazer, simplesmente porque você está ofuscando as informações da exceção e dificultando desnecessariamente a filtragem de exceções na pilha de chamadas. O único momento em que você deve capturar um tipo de exceção e lançar outro é quando está implementando uma camada de abstração e precisa transformar um tipo de exceção específico do provedor (por exemplo, SqlException versus XmlException) em um mais genérico (por exemplo, DataLoadingException).
Jammycakes 18/10/10
3
@dark_perfect, você deve verificar esse argumento antecipadamente, no início do método e lançar a ArgumentNullException lá (falha rápida).
Andrei Bozantan
56

O C # (antes do C # 6) não suporta "exceções filtradas" do CIL, o que o VB faz; portanto, no C # 1-5, uma razão para lançar uma exceção é que você não possui informações suficientes no momento da captura () para determinar se você realmente deseja capturar a exceção.

Por exemplo, no VB você pode fazer

Try
 ..
Catch Ex As MyException When Ex.ErrorCode = 123
 .. 
End Try

... que não trataria MyExceptions com diferentes valores de ErrorCode. No C # anterior à v6, você precisaria capturar e lançar novamente a MyException se o ErrorCode não fosse 123:

try 
{
   ...
}
catch(MyException ex)
{
    if (ex.ErrorCode != 123) throw;
    ...
}

Desde o C # 6.0, você pode filtrar como no VB:

try 
{
  // Do stuff
} 
catch (Exception e) when (e.ErrorCode == 123456) // filter
{
  // Handle, other exceptions will be left alone and bubble up
}
bzlm
fonte
2
Dave, mas (pelo menos em java) você não lançaria uma MyException "genérica", definiria um tipo de exceção ESPECÍFICO e a lançaria, permitindo que ela fosse diferenciada por tipo no bloco catch ... Mas sim , se você não é o arquiteto da exceção (estou pensando no SQLException do JDBC (Java novamente) aqui, que é nojento genérico e expõe o método getErrorCode () ... Hmmm ... Você tem razão, é exatamente isso Eu acho que há uma maneira melhor de fazê-lo, sempre que possível Companheiro dos elogios que eu aprecio o seu tempo, um monte Keith....
corlettk
1
Bem, a pergunta é "Por que capturar e repetir a exceção em C #?", E essa é uma resposta. =] ... e mesmo com exceções especializadas, os Filtros de exceção fazem sentido: considere o caso em que você está, digamos, lidando com um SqlTimeoutException e um SqlConnectionResetException, que são SqlException. Os filtros de exceção permitem capturar um SqlException apenas quando é um desses dois; portanto, em vez de sobrecarregar sua tentativa / captura com manipulação idêntica para esses dois, você pode "capturar SqlException ex quando ex é SqlTimeoutException ou ex é SqlConnectionResetException". (Eu não sou Dave btw)
bzlm
3
As exceções filtradas estão chegando no C # 6!
Sir Crispalot 07/07
14

Meu principal motivo para ter código como:

try
{
    //Some code
}
catch (Exception e)
{
    throw;
}

é para que eu possa ter um ponto de interrupção na captura, que tenha um objeto de exceção instanciada. Eu faço muito isso durante o desenvolvimento / depuração. Obviamente, o compilador me dá um aviso sobre todos os e não utilizados e, idealmente, eles devem ser removidos antes da compilação do release.

Eles são bons durante a depuração embora.

Peter Mortensen
fonte
1
Sim, eu vou pagar isso, mas sim, você não gostaria de ver que em publicou código ... ergo: Eu teria vergonha de publicá-lo ;-)
corlettk
25
Na verdade, isso não é necessário - no Visual Studio, você pode definir o depurador para quebrar quando uma exceção é lançada e exibir os detalhes da exceção em uma janela do inspetor para você.
21139 jammycakes
8
Se você quiser usar SOMENTE algum código durante a depuração, use #if DEBUG ... #endif e não precisará remover essas linhas #
Michael Freidgeim
1
Sim, eu já fiz isso algumas vezes. De vez em quando alguém escapará para um lançamento. @jammycakes O problema com a quebra do Visual Studio na exceção é que, às vezes, a exceção que eu quero não é a única (ou mesmo apenas uma de seu tipo) sendo lançada. Ainda não conhece uma condição de ponto de interrupção com "quebra se ignorada por exceção". Até lá, isso continuará sendo útil. Michael Freidgeim: #if DEBUGem torno de ambos os try {e} catch () {...} é um pouco bagunçado e, francamente, me deixa enjoado ... O pré-processador, em geral, não é meu amigo.
11

Um motivo válido para relançar exceções pode ser o fato de você desejar adicionar informações à exceção ou talvez agrupar a exceção original em uma criação sua:

public static string SerializeDTO(DTO dto) {
  try {
      XmlSerializer xmlSer = new XmlSerializer(dto.GetType());
      StringWriter sWriter = new StringWriter();
      xmlSer.Serialize(sWriter, dto);
      return sWriter.ToString();
  }
  catch(Exception ex) {
    string message = 
      String.Format("Something went wrong serializing DTO {0}", DTO);
    throw new MyLibraryException(message, ex);
  }
}
edosoft
fonte
Thanx, sim, o empacotamento de exceção (especialmente encadeado) é perfeitamente sadio ... o que não é sensato é capturar uma exceção apenas para que você possa descartar o rastreamento da pilha, ou pior, comê-lo.
Corlettk 19/05/09
10

Isso não é exatamente equivalente a não lidar com exceções?

Não exatamente, não é o mesmo. Redefine o rastreamento de pilha da exceção. Embora eu concorde que isso provavelmente seja um erro e, portanto, um exemplo de código incorreto.

Arjan Einbu
fonte
8

Você não quer jogar ex - pois isso perderá a pilha de chamadas. Consulte Tratamento de exceções (MSDN).

E sim, o try ... catch não está fazendo nada útil (além de perder a pilha de chamadas - então é realmente pior - a menos que, por algum motivo, você não queira expor essas informações).

Duncan
fonte
Você não perde a pilha de chamadas inteira quando usa throw ex, apenas perde a parte da pilha de chamadas a partir do ponto em que a exceção ocorreu mais acima da pilha de chamadas. Mas você mantém a pilha de chamadas do método que lançou a exceção para onde o cliente a chamou. Na verdade, pode haver casos de uso em que você usaria isso, ou as pessoas boas da Microsoft não teriam permitido. Dito isto, eu não usei. Outra questão a lembrar é que lançar exceções é caro. Faça isso apenas por uma razão muito justificável. Logging Eu acho que seria justificável, etc.
Charles Owen
5

Um ponto que as pessoas não mencionaram é que, embora as linguagens .NET não façam uma distinção adequada, a questão de saber se alguém deve agir quando ocorre uma exceção e se deve resolver , são realmente perguntas distintas. Existem muitos casos em que uma ação deve ser tomada com base em exceções que não temos esperança de resolver, e em alguns casos tudo o que é necessário para "resolver" uma exceção é relaxar a pilha até um determinado ponto - nenhuma ação adicional é necessária. .

Por causa do senso comum de que só se deve "capturar" as coisas que se pode "manipular", muito código que deve agir quando ocorrem exceções, não. Por exemplo, muitos códigos adquirem um bloqueio, colocam o objeto guardado "temporariamente" em um estado que viole seus invariantes, em seguida, colocam o objeto em um estado legítimo e liberam o bloqueio antes que mais alguém possa ver o objeto. Se ocorrer uma exceção enquanto o objeto estiver em um estado perigosamente inválido, a prática comum é liberar o bloqueio com o objeto ainda nesse estado. Um padrão muito melhor seria ter uma exceção que ocorra enquanto o objeto estiver em uma condição "perigosa" invalidará expressamente o bloqueio, para que qualquer tentativa futura de adquiri-lo falhe imediatamente.

Na maioria das linguagens .NET, a única maneira de o código executar uma ação com base em uma exceção é catchfazê - lo (mesmo sabendo que não resolverá a exceção), execute a ação em questão e depois re throw). Outra abordagem possível, se o código não se importa com a exceção lançada, é usar um oksinalizador com um try/finallybloco; defina o oksinalizador como falseantes do bloco e trueantes que o bloco saia e antes de qualquer um returnque esteja dentro do bloco. Em seguida, finallyassuma que, se oknão estiver definido, uma exceção deve ter ocorrido. Essa abordagem é semanticamente melhor que a catch/ throw, mas é feia e é menos sustentável do que deveria.

supercat
fonte
5

Isso pode ser útil quando sua programação funciona para uma biblioteca ou dll.

Essa estrutura de relançamento pode ser usada para redefinir propositalmente a pilha de chamadas para que, em vez de ver a exceção lançada de uma função individual dentro da função, você obtenha a exceção da própria função.

Eu acho que isso é usado apenas para que as exceções lançadas sejam mais limpas e não entrem nas "raízes" da biblioteca.

Jackson Tarisa
fonte
3

Um motivo possível para capturar é impedir que filtros de exceção mais profundos da pilha sejam filtrados ( link antigo aleatório ). Mas é claro que, se essa era a intenção, haveria um comentário dizendo isso.

Brian
fonte
Eu não entendi o que você estava falando até ler o link ... e ainda não sei exatamente o que você está falando ... por eu não estar totalmente familiarizado com o VB.NET. Eu acho que resulta na soma sendo relatada como "inconsistente", certo? ... Eu sou um GRANDE fã de métodos estáticos .. além de simples, há menos chance de inconsistência se você separar a configuração de atributos do código qual faz o trabalho real. A pilha é "auto-limpante".
Corlettk
3
As pessoas esperam que, quando escrevem "tente {Foo ();} finalmente {Bar ();}"), nada funcione entre Foo e Bar. Mas isso não é verdade; se o chamador tiver adicionado um filtro de exceção e não houver 'catch' interveniente e Foo () for lançado, algum outro código aleatório do chamador será executado antes que o finalmente (Bar) seja executado. Isso é muito ruim se você violar invariantes ou aumentar a segurança, esperando que eles sejam 'imediatamente' restaurados ao normal finalmente e nenhum outro código verá a mudança temporária.
287 Brian Brian
3

Depende do que você está fazendo no bloco catch e se deseja passar o erro para o código de chamada ou não.

Pode-se dizer Catch io.FileNotFoundExeption ex e usar um caminho de arquivo alternativo ou algo parecido, mas ainda assim ativar o erro.

Também fazer em Throwvez de Throw Expermite manter o rastreamento completo da pilha. Throw ex reinicia o rastreamento de pilha da instrução throw (espero que faça sentido).

Pondidum
fonte
3

Enquanto muitas das outras respostas fornecem bons exemplos de por que você pode querer capturar uma exceção de relançamento, ninguém parece ter mencionado um cenário 'finalmente'.

Um exemplo disso é onde você tem um método no qual define o cursor (por exemplo, para um cursor de espera), o método possui vários pontos de saída (por exemplo, se () retorna;) e deseja garantir que o cursor seja redefinido no fim do método.

Para fazer isso, você pode agrupar todo o código em uma tentativa / captura / finalmente. Finalmente, defina o cursor de volta ao cursor direito. Para que você não esconda nenhuma exceção válida, repita-a novamente.

try
{
    Cursor.Current = Cursors.WaitCursor;
    // Test something
    if (testResult) return;
    // Do something else
}
catch
{
    throw;
}
finally
{
     Cursor.Current = Cursors.Default;
}
statler
fonte
1
Foi catchuma parte obrigatória do try...finallyhistórico ou desempenha um papel funcional neste exemplo? - Apenas verifiquei duas vezes e posso usar try {} finally {}sem o bloco de captura.
Sebi
2

No exemplo do código que você postou, de fato, não faz sentido capturar a exceção, pois não há nada feito na captura, apenas refazer a ação; na verdade, causa mais mal do que bem, pois a pilha de chamadas é perdida. .

Você, no entanto, capturaria uma exceção para fazer alguma lógica (por exemplo, fechar a conexão sql do bloqueio de arquivo ou apenas alguns registros) no caso de uma exceção, devolvê-la ao código de chamada para lidar. Isso seria mais comum em uma camada de negócios do que no código de front-end, pois você pode querer que o codificador que implementa sua camada de negócios lide com a exceção.

Para reiterar o ponto NÃO há como capturar a exceção no exemplo que você postou. NÃO faça assim!

Sheff
fonte
1

Desculpe, mas muitos exemplos de "design aprimorado" ainda cheiram horrivelmente ou podem ser extremamente enganadores. Tendo tentado {} catch {log; jogar} é totalmente inútil. O registro de exceção deve ser feito em um local central dentro do aplicativo. de qualquer maneira, as exceções aumentam o rastreamento de pilha, por que não registrá-las em algum lugar próximo às bordas do sistema?

Deve-se tomar cuidado ao serializar seu contexto (por exemplo, DTO em um exemplo) apenas na mensagem de log. Ele pode facilmente conter informações confidenciais que talvez você não queira alcançar nas mãos de todas as pessoas que podem acessar os arquivos de log. E se você não adicionar nenhuma informação nova à exceção, realmente não vejo o ponto de agrupar as exceções. O bom e velho Java tem algum argumento para isso, exige que o chamador saiba que tipo de exceções se deve esperar ao chamar o código. Como você não possui isso no .NET, o agrupamento não é bom em pelo menos 80% dos casos que eu já vi.


fonte
Obrigado pelo seu pensamento Joe. Em Java (e C #, suponho), eu adoraria ver uma anotação em nível de classe @FaultBoundary que força TODAS as exceções (incluindo tipos de exceção não verificados) a serem capturadas ou declaradas para serem lançadas. Eu usaria essa anotação em interfaces públicas de cada camada arquitetural. Portanto, a interface @FaultBoundary ThingDAO não poderia vazar detalhes da implementação, como SQLExceptions, NPE ou AIOB. Em vez disso, o rastreamento de pilha "causal" seria registrado e uma DAOSystemException seria lançada ... Defino a exceção do sistema como "permanentemente fatal".
Corlettk 03/12/2009
5
Existem várias razões para capturar, registrar e depois reproduzir novamente. Especificamente, se o método com o log de captura tiver informações que você perderá quando estiver fora do método. O erro pode ser tratado posteriormente, mas não registrado, e você perdeu informações sobre defeitos no sistema.
Andy
1
É aqui que a propriedade Data da classe Exception é útil - capturando todas as informações locais para log genérico. Este artigo originalmente chamou minha atenção: blog.abodit.com/2010/03/…
McGuireV10
1

Além do que os outros disseram, veja minha resposta a uma pergunta relacionada que mostra que capturar e relançar não é um não-op (está no VB, mas parte do código pode ser invocada em C # pelo VB).

erikkallen
fonte
Embora esse link possa responder à pergunta, é melhor incluir aqui as partes essenciais da resposta e fornecer o link para referência. As respostas somente para links podem se tornar inválidas se a página vinculada for alterada. - Do comentário
LHIOUI
@HamzaLH, concordo que não é uma resposta bem escrita, mas tem informações diferentes de outras respostas e votos positivos. Então, eu não entendo, por que você sugere excluí-lo? "Respostas curtas que estão no tópico e dão uma solução ainda são respostas." De meta.stackexchange.com/questions/226258/…
Michael Freidgeim
este é resposta só-link
LHIOUI
1. As respostas somente de link devem ser alteradas para comentários, não excluídas. 2. É uma referência a outra pergunta do SO, não ao site externo, que considerou menos provável que fosse interrompido ao longo do tempo. 3. Ele tem uma descrição extra, que o torna não apenas "link" - consulte meta.stackexchange.com/questions/225370/…
Michael Freidgeim
1

A maioria das respostas fala sobre o cenário catch-log-rehrow.

Em vez de escrevê-lo em seu código, considere usar AOP, em particular Postsharp.Diagnostic.Toolkit com OnExceptionOptions IncludeParameterValue e IncludeThisArgument

Michael Freidgeim
fonte
Embora esse link possa responder à pergunta, é melhor incluir aqui as partes essenciais da resposta e fornecer o link para referência. As respostas somente para links podem se tornar inválidas se a página vinculada for alterada. - Do comentário
Tony Dong
@ TonyDong, eu concordo que não é uma resposta bem escrita, mas tem informações diferentes de outras respostas e votos positivos. Então eu não entendo, por que você sugere excluí-lo? BTW, o link 5 anos depois ainda é válido. "Respostas curtas que estão no tópico e dão uma solução ainda são respostas." De meta.stackexchange.com/questions/226258/…
Michael Freidgeim
O Stackoverflow possui apenas esta sugestão.
Tony Dong
@TonyDong, se a resposta não é absolutamente inútil, você deve escolher “Looks OK”
Michael Freidgeim
0

A reconfiguração de exceções via throwé útil quando você não possui um código específico para lidar com exceções atuais ou nos casos em que você tem uma lógica para lidar com casos de erro específicos, mas deseja pular todos os outros.

Exemplo:

string numberText = "";
try
{
    Console.Write("Enter an integer: ");
    numberText = Console.ReadLine();
    var result = int.Parse(numberText);

    Console.WriteLine("You entered {0}", result);
}
catch (FormatException)
{
    if (numberText.ToLowerInvariant() == "nothing")
    {
        Console.WriteLine("Please, please don't be lazy and enter a valid number next time.");
    }
    else
    {
        throw;
    }
}    
finally
{
    Console.WriteLine("Freed some resources.");
}
Console.ReadKey();

No entanto, há também outra maneira de fazer isso, usando cláusulas condicionais em blocos catch:

string numberText = "";
try
{
    Console.Write("Enter an integer: ");
    numberText = Console.ReadLine();
    var result = int.Parse(numberText);

    Console.WriteLine("You entered {0}", result);
}
catch (FormatException) when (numberText.ToLowerInvariant() == "nothing")
{
    Console.WriteLine("Please, please don't be lazy and enter a valid number next time.");
}    
finally
{
    Console.WriteLine("Freed some resources.");
}
Console.ReadKey();

Esse mecanismo é mais eficiente do que lançar uma exceção, porque o tempo de execução do .NET não precisa recriar o objeto de exceção antes de lançá-lo novamente.

Arsen Khachaturyan
fonte