Capturar exceções gerais é realmente uma coisa ruim?

57

Normalmente, concordo com a maioria dos avisos de análise de código e tento segui-los. No entanto, estou tendo mais dificuldade com este:

CA1031: Não captura tipos de exceção gerais

Eu entendo a lógica dessa regra. Mas, na prática, se eu quero executar a mesma ação, independentemente da exceção lançada, por que lidaria com cada uma delas especificamente? Além disso, se eu lidar com exceções específicas, e se o código que estou chamando for alterado para lançar uma nova exceção no futuro? Agora eu tenho que mudar meu código para lidar com essa nova exceção. Considerando que se eu simplesmente peguei Exceptionmeu código não precisa mudar.

Por exemplo, se Foo chama Bar e Foo precisa parar de processar, independentemente do tipo de exceção lançada por Bar, há alguma vantagem em ser específico sobre o tipo de exceção que estou capturando?

Talvez um exemplo melhor:

public void Foo()
{
    // Some logic here.
    LogUtility.Log("some message");
}

public static void Log()
{
    try
    {
        // Actual logging here.
    }
    catch (Exception ex)
    {
        // Eat it. Logging failures shouldn't stop us from processing.
    }
}

Se você não capturar uma exceção geral aqui, precisará capturar todos os tipos de exceção possíveis. Patrick tem um bom argumento que OutOfMemoryExceptionnão deve ser tratado dessa maneira. E daí se eu quiser ignorar todas as exceções OutOfMemoryException?

Bob Horn
fonte
12
Que tal OutOfMemoryException? O mesmo código de manipulação que tudo o resto?
Patrick
@Patrick Bom ponto. Eu adicionei um novo exemplo na minha pergunta para cobrir sua pergunta.
Bob Horn
11
Capturar exceções gerais é tão ruim quanto fazer declarações gerais sobre o que todos deveriam fazer. Geralmente, não existe uma abordagem única para tudo.
4
@Patrick que seria OutOfMemoryError, que é separado da Exceptionárvore de herança por essa mesma razão
Darkhogg
E as exceções do teclado, como Ctrl-Alt-Del?
MAWG

Respostas:

33

Essas regras geralmente são uma boa idéia e, portanto, devem ser seguidas.

Mas lembre-se de que estas são regras genéricas. Eles não cobrem todas as situações. Eles cobrem as situações mais comuns. Se você tem uma situação específica e pode argumentar que sua técnica é melhor (e deve poder escrever um comentário no código para articular seu argumento para fazê-lo), faça-o (e faça uma revisão por pares).

No lado contrário da discussão.

Não vejo o seu exemplo acima como uma boa situação para isso. Se o sistema de registro estiver falhando (presumivelmente registrando outra exceção), provavelmente não quero que o aplicativo continue. Saia e imprima a exceção na saída para que o usuário possa ver o que aconteceu.

Martin York
fonte
2
Concordar. No mínimo , agora que o log regular está offline, faça algum tipo de provisão que fornecerá algum tipo de saída de depuração para STDERR, se nada mais.
Shadur 10/09/12
6
Votei nos dois, mas acho que você está pensando como desenvolvedores, não como usuários. Um usuário geralmente não se importa se o log está acontecendo, desde que o aplicativo seja utilizável.
MAWG
11
Interessante, como o log4net explicitamente não deseja interromper o aplicativo apenas porque o log falhou. E acho que depende do que você está registrando; auditorias de segurança, com certeza, acabam com o processo. Mas logs de depuração? Isso não é importante.
Andy
11
@ Andy: Sim, tudo depende do que você está fazendo.
Martin York
2
Por que você não os entende? E você não deve se esforçar para fazer isso?
MAWG
31

Sim, capturar exceções gerais é uma coisa ruim. Uma exceção geralmente significa que o programa não pode fazer o que você pediu.

Existem alguns tipos de exceções que você pode manipular:

  • Exceções fatais : falta de memória, excesso de pilha, etc. Alguma força sobrenatural acabou de atrapalhar o seu universo e o processo já está morrendo. Você não pode melhorar a situação, então desista
  • Exceção gerada devido a erros no código que você está usando : não tente manipulá-los, mas sim corrija a origem do problema. Não use exceções para controle de fluxo
  • Situações excepcionais : lide apenas com exceção nesses casos. Aqui podemos incluir: cabo de rede desconectado, conexão à Internet parou de funcionar, falta de permissões, etc.

Ah, e como regra geral: se você não souber o que fazer com uma exceção, se a capturar, é melhor simplesmente falhar rapidamente (passe a exceção para o chamador e deixe-o lidar com isso)

Victor Hurdugaci
fonte
11
E o caso específico da pergunta? Se houver um erro no código de log, provavelmente não há problema em ignorar a exceção, porque o código que realmente importa não deve ser afetado por isso.
svick
4
Por que não corrigir o bug no código de log?
Victor Hurdugaci
3
Victor, provavelmente não é um bug. Se o disco no qual o log fica sem espaço, isso é um bug no código de log?
Carson63000
4
@ Carson63000 - bem, sim. Os logs não estão sendo gravados, portanto: bug. Implemente logs rotativos de tamanho fixo.
detly 10/09/12
5
Essa é a melhor resposta, mas falta um aspecto crucial: segurança . Há algumas exceções que ocorrem como resultado dos bandidos tentando atualizar seu programa. Se uma exceção for lançada e você não puder manipular, não poderá mais necessariamente confiar nesse código. Ninguém gosta de ouvir que escreveu o código que deixar os caras maus no.
riwalk
15

O loop externo mais alto deve ter um destes para imprimir tudo o que puder e, em seguida, sofrer uma morte horrível, violenta e barulhenta (como isso não deveria acontecer e alguém precisa ouvir).

Caso contrário, você geralmente deve ter muito cuidado, pois provavelmente não antecipou tudo o que poderia acontecer neste local e, portanto, provavelmente não o tratará corretamente. Seja o mais específico possível, para pegar apenas aqueles que sabe que vão acontecer e deixar que os que não foram vistos antes borbulhem até a morte barulhenta acima mencionada.


fonte
11
Eu concordo com isso em teoria. Mas e o meu exemplo de registro acima? E se você simplesmente deseja ignorar as exceções de log e continuar? Você precisa capturar todas as exceções que possam ser lançadas e lidar com cada uma delas, deixando a exceção mais séria surgir?
Bob Horn
6
@ BobHorn: Duas maneiras de ver isso. Sim, você pode dizer que o log não é particularmente importante e, se falhar, não interromperá o aplicativo. E esse é um ponto bastante justo. Mas e se o seu aplicativo falhar no registro por cinco meses e você não tiver ideia? Eu já vi isso acontecer. E então precisávamos dos logs. Desastre. Eu sugeriria que fazer tudo o que você possa pensar para interromper a falha do registro é melhor do que ignorá-lo quando isso ocorre. (Mas você não vai ficar muito de um consenso sobre isso.)
PDR
@pdr No meu exemplo de log, normalmente eu enviava um alerta por email. Ou, temos sistemas para monitorar logs e, se um log não estiver sendo preenchido ativamente, nossa equipe de suporte receberá um alerta. Portanto, ficamos cientes do problema do registro de outras maneiras.
9789 Bob Horn
@BobHorn, seu exemplo de log é bastante artificial - a maioria das estruturas de log que eu conheço, se esforça para não lançar nenhuma exceção e não seria invocada assim (pois tornaria inútil qualquer "declaração de log na linha X no arquivo Y") )
@pdr Eu levantei a questão na lista de logback (Java) sobre como fazer com que a estrutura de log interrompa o aplicativo se a configuração de log falhar, simplesmente porque é tão importante que tenhamos os logs e qualquer falha silenciosa é inaceitável. Ainda está pendente uma boa solução.
9

Não é que seja ruim, é apenas que capturas específicas são melhores. Quando você é específico, significa que você realmente entende, mais concretamente, o que seu aplicativo está fazendo e tem mais controle sobre ele. Em geral, se você se deparar com uma situação em que simplesmente captura uma exceção, registra e continua, provavelmente há algumas coisas ruins que estão acontecendo de qualquer maneira. Se você está capturando especificamente as exceções que sabe que um bloco ou método de código pode lançar, há uma probabilidade maior de recuperar, em vez de apenas registrar e esperar o melhor.

Ryan Hayes
fonte
5

As duas possibilidades não são mutuamente exclusivas.

Em uma situação ideal, você capturaria todos os tipos possíveis de exceção que seu método poderia gerar, os trataria por exceção e, no final, adicionaria uma catchcláusula geral para capturar exceções futuras ou desconhecidas. Dessa forma, você obtém o melhor dos dois mundos.

try
{
    this.Foo();
}
catch (BarException ex)
{
    Console.WriteLine("Foo has barred!");
}
catch (BazException ex)
{
    Console.WriteLine("Foo has bazzed!");
}
catch (Exception ex)
{
    Console.WriteLine("Unknown exception on Foo!");
    throw;
}

Lembre-se de que, para capturar as exceções mais específicas, você deve colocá-las em primeiro lugar.

Editar: por motivos declarados nos comentários, foi adicionada uma repetição na última captura.

Rotem
fonte
2
Esperar. Por que você deseja registrar uma exceção desconhecida para console e continuar? Se é uma exceção que você nem sabia que poderia ser lançada pelo código, provavelmente está falando de uma falha no sistema. Continuar nesse cenário é provavelmente uma péssima idéia.
pdr
11
@pdr Certamente você percebe que é um exemplo artificial. O objetivo era demonstrar a captura de exceções específicas e gerais, não como lidar com elas.
Rotem
@ Rotem Eu acho que você tem uma boa resposta aqui. Mas pdr tem um ponto. O código, como está, faz parecer que pegar e continuar é bom. Alguns iniciantes, vendo este exemplo, podem pensar que é uma boa abordagem.
Bob Horn
Inverso ou não, sua resposta está errada. Assim que você começa a capturar exceções como Memória insuficiente e Disco cheio e apenas engoli-las, está provando por que a captura de exceções gerais é de fato ruim.
pdr
11
Se você capturar uma exceção geral apenas para registrá-la, deverá repeti-la novamente após o registro.
Victor Hurdugaci 9/09/12
5

Estive pensando sobre o mesmo recentemente, e minha conclusão preliminar é que a mera pergunta surge porque a hierarquia do .NET Exception está severamente bagunçada.

Pegue, por exemplo, o modesto ArgumentNullExceptionque pode ser um candidato razoável para uma exceção que você não deseja capturar, porque tende a indicar um bug no código, em vez de um erro legítimo de tempo de execução. Ah sim. O mesmo acontece com a NullReferenceExceptionexceção de que NullReferenceExceptionderiva SystemExceptiondiretamente, portanto, não há nenhum balde no qual você possa colocar todos os "erros lógicos" para capturar (ou não capturar).

Depois, há o problema principal da IMNSHO de SEHExceptionderivar (via ExternalException) SystemExceptione, assim, torná-la "normal"SystemException Quando você obtém umaSEHException, deseja escrever um dump e terminar o mais rápido possível - e começando pelo .NET 4 pelo menos alguns As exceções SEH são consideradas Exceções de Estado Corruptas que não serão capturadas. Uma coisa boa e um fato que torna aCA1031regra ainda mais inútil, porque agora o seu preguiçosocatch (Exception)não pegará esses piores tipos de qualquer maneira.

Então, parece que as outras coisas do Framework derivam de maneira inconsistente, Exceptiondireta ou via SystemException, fazendo qualquer tentativa de agrupar cláusulas de captura por algo como a gravidade da gravidade.

Há uma peça do Sr. Lippert da C#fama, chamada Vexing Exception , em que ele expõe algumas categorias úteis de exceções: você pode argumentar que deseja capturar apenas "exógenas", exceto ... C#a linguagem e o design do .NET as exceções da estrutura tornam impossível "capturar apenas as exógenas" de maneira sucinta. (e, por exemplo, OutOfMemoryExceptionpode muito bem ser um erro recuperável totalmente normal para uma API que precisa alocar buffers de alguma forma grandes)

O ponto principal para mim é que, da maneira como C#os blocos catch funcionam e como a hierarquia de exceções do Framework é projetada, a regra CA1031está além de totalmente inútil . Ele finge para ajudar com o problema de raiz de "não engolir exceções", mas engolir exceções tem zero a ver com o que você pegar, mas com o que você , em seguida, fazer:

Existem pelo menos quatro maneiras de lidar legitimamente com um pego Exception, e CA1031apenas parece lidar superficialmente com um deles (a saber, o caso de re-lançamento).


Como uma observação lateral, há um recurso do C # 6 chamado Filtros de Exceção que se tornará CA1031um pouco mais válido novamente desde então, você poderá filtrar corretamente as exceções que deseja capturar, e há menos motivos para escrever um filtro não filtrado catch (Exception).

Martin Ba
fonte
11
Um grande problema OutOfMemoryExceptioné que não existe uma maneira agradável de o código ter certeza de que "apenas" indica a falha de uma alocação específica que a pessoa estava preparada para falhar. É possível que alguma outra exceção mais séria tenha sido lançada e tenha OutOfMemoryExceptionocorrido durante o desenrolamento da pilha dessa outra exceção. O Java pode ter se atrasado para a parte com sua "tentativa com recursos", mas lida com exceções durante o desenrolamento da pilha um pouco melhor que o .NET.
supercat
11
@ supercat - é verdade, mas isso é praticamente verdade para qualquer outra exceção geral do sistema, quando as coisas dão errado em qualquer bloco finalmente.
Martin Ba
11
Isso é verdade, mas pode-se (e geralmente deveria) proteger os finallyblocos contra exceções que, realisticamente, se pode esperar que ocorram de tal maneira que as exceções originais e novas sejam registradas. Infelizmente, isso exigirá muitas vezes alocação de memória, o que pode causar falhas próprias.
supercat
4

O manuseio de exceção de Pokemon (tem que pegar todos!) Certamente nem sempre é ruim. Quando você expõe um método a um cliente, especialmente um usuário final, geralmente é melhor capturar tudo e qualquer coisa, em vez de travar e queimar seu aplicativo.

Geralmente, eles devem ser evitados onde você puder. A menos que você possa executar uma ação específica com base no tipo de exceção, é melhor não tratá-la e permitir que a exceção borbulhe em vez de engolir a exceção ou manipulá-la incorretamente.

Dê uma olhada nesta resposta para mais informações.

Tom Squires
fonte
11
Essa situação (quando um desenvolvedor deve ser um Pokemon Trainer) aparece quando você está criando um software inteiro: você criou as camadas, as conexões com o banco de dados e a GUI do usuário. Seu aplicativo deve se recuperar de situações como perda de conectividade, pasta do usuário excluída, dados corrompidos etc. Os usuários finais não gostam de aplicativos que travam e queimam. Eles gostam de aplicativos que podem funcionar em um computador com explosão e explosão!
Broken_Window 03/03
Eu não tinha pensado nisso até agora, mas em Pokemon, geralmente é assumido que, 'pegando todos', você quer exatamente um de cada um e precisa lidar com pegá-lo especificamente, o que é o oposto do uso comum dessa frase. entre programadores.
Magus
2
@ Magus: Com um método como o LoadDocument(), é essencialmente impossível identificar todas as coisas que podem dar errado, mas 99% das exceções que podem ser lançadas significarão simplesmente "Não foi possível interpretar o conteúdo de um arquivo com o nome dado como um documento; lide com ele ". Se alguém tentar abrir algo que não é um arquivo de documento válido, isso não deve travar o aplicativo e matar outros documentos abertos. O tratamento de erros de Pokemon nesses casos é feio, mas não conheço boas alternativas.
21814
@ supercat: Eu estava apenas fazendo uma observação lingüística. No entanto, não acho que o conteúdo de arquivo inválido pareça algo que deva gerar mais de um tipo de exceção.
Magus
11
@ Magus: Pode lançar todos os tipos de exceções - esse é o problema. Muitas vezes, é muito difícil prever todos os tipos de exceções que podem ser lançadas como consequência de dados inválidos em um arquivo. Se alguém não usa o manuseio do PokeMon, corre-se o risco de um aplicativo morrer porque, por exemplo, o arquivo que estava sendo carregado continha uma sequência de dígitos excepcionalmente longa em um local que exigia um número inteiro de 32 bits no formato decimal e ocorreu um estouro numérico em a lógica de análise.
Supercat
1

A captura de exceção geral é ruim porque deixa seu programa em um estado indefinido. Você não sabe onde as coisas deram errado e não sabe o que o seu programa realmente fez ou não fez.

Onde eu permitiria pegar tudo é ao fechar um programa. Contanto que você possa limpar tudo bem. Nada tão irritante quanto um programa que você fecha, que apenas gera uma caixa de diálogo de erro que não faz nada além de ficar sentada ali, sem ir embora e impedindo o fechamento do computador.

Em um ambiente distribuído, seu método de log pode sair pela culatra: capturar uma exceção geral pode significar que seu programa ainda mantém um bloqueio no arquivo de log, impedindo que outros usuários façam logs.

Pieter B
fonte
0

Eu entendo a lógica dessa regra. Mas, na prática, se eu quero executar a mesma ação, independentemente da exceção lançada, por que lidaria com cada uma delas especificamente?

Como outros já disseram, é realmente difícil (se não impossível) imaginar alguma ação que você deseja executar independentemente da exceção lançada. Apenas um exemplo são situações em que o estado do programa foi corrompido e qualquer processamento adicional pode levar a problemas (esta é a lógica por trás Environment.FailFast).

Além disso, se eu lidar com exceções específicas, e se o código que estou chamando for alterado para lançar uma nova exceção no futuro? Agora eu tenho que mudar meu código para lidar com essa nova exceção. Enquanto que, se eu simplesmente pegasse Exception, meu código não precisa ser alterado.

Para o código do hobby, é bom entender Exception, mas para o código de nível profissional, a introdução de um novo tipo de exceção deve ser tratada com o mesmo respeito que uma alteração na assinatura do método, ou seja, ser considerada uma alteração de quebra. Se você se inscrever neste ponto de vista, será imediatamente óbvio que voltar a alterar (ou verificar) o código do cliente é o único curso de ação correto.

Por exemplo, se Foo chama Bar e Foo precisa parar de processar, independentemente do tipo de exceção lançada por Bar, há alguma vantagem em ser específico sobre o tipo de exceção que estou capturando?

Claro, porque você não estará capturando apenas exceções geradas Bar. Também haverá exceções que os clientes de Bar ou mesmo o tempo de execução poderão lançar durante o tempo que Barestiver na pilha de chamadas. Um bem escrito Bardeve definir seu próprio tipo de exceção, se necessário, para que os chamadores possam capturar especificamente as exceções emitidas por si mesmas.

E se eu quiser ignorar todas as exceções, exceto OutOfMemoryException?

IMHO esta é a maneira errada de pensar sobre o tratamento de exceções. Você deve estar operando em listas brancas (capturar os tipos de exceção A e B), não em listas negras (capturar todas as exceções, exceto X).

Jon
fonte
Geralmente, é possível especificar uma ação que deve ser executada para todas as exceções que indiquem que uma tentativa de operação falhou sem efeito colateral e / ou implicação adversa sobre o estado do sistema. Por exemplo, se um usuário seleciona um arquivo de documento para carregar, um programa passa seu nome para o método "criar objeto de documento com dados de um arquivo de disco", e esse método falha por algum motivo específico que o aplicativo não havia previsto (mas que atende aos critérios acima), o comportamento adequado deve ser a exibição de uma mensagem "Não é possível carregar o documento" em vez de travar o aplicativo.
Supercat
Infelizmente, não existe um meio padrão pelo qual uma exceção possa indicar a coisa mais importante que o código do cliente gostaria de saber - se isso implica algo ruim no estado do sistema além do que seria implícito pela incapacidade de executar a operação solicitada. Portanto, mesmo que as situações em que todas as exceções devam ser tratadas sejam raras, há muitas situações em que muitas exceções devem ser tratadas da mesma maneira, e não há critério que seja satisfeito por todas essas exceções também seria satisfeito por alguns raros que deve ser tratado de maneira diferente.
Supercat
0

Talvez . Há exceções para todas as regras e nenhuma regra deve ser seguida acriticamente. Seu exemplo pode ser um dos casos em que faz sentido engolir todas as exceções. Por exemplo, se você deseja adicionar rastreio a um sistema crítico de produção e deseja ter certeza absoluta de que as alterações não atrapalham a tarefa principal do aplicativo.

No entanto , você deve pensar cuidadosamente sobre os possíveis motivos da falha antes de decidir ignorá-los silenciosamente. Por exemplo, e se o motivo da exceção for:

  • um erro de programação no método de registro que faz com que sempre lance uma exceção específica
  • um caminho inválido para o arquivo de log na configuração
  • o disco está cheio

Você não deseja ser notificado imediatamente de que esse problema está presente para poder corrigi-lo? Engolir a exceção significa que você nunca sabe que algo deu errado.

Alguns problemas (como o disco está cheio) também podem causar falhas em outras partes do aplicativo - mas essa falha não está registrada agora, então você nunca sabe!

JacquesB
fonte
0

Eu gostaria de abordar isso de uma perspectiva lógica e não técnica,

Agora eu tenho que mudar meu código para lidar com essa nova exceção.

Bem, alguém teria que lidar com isso. Essa é a ideia. Os escritores do código da biblioteca seriam prudentes ao adicionar novos tipos de exceção, porque, como é provável que eles quebrem clientes, você não deve encontrar isso com muita frequência.

Sua pergunta é basicamente "E se eu não me importo com o que deu errado? Eu realmente tenho que passar pelo trabalho de descobrir o que era?"

Aqui está a parte da beleza: não, você não.

"Então, posso apenas olhar para o outro lado e fazer com que qualquer coisa desagradável apareça varrida para debaixo do tapete automaticamente e acabe com isso?"

Não, também não é assim que funciona.

O ponto é que a coleção de possíveis exceções é sempre maior que a coleção que você espera e está interessada no contexto do seu pequeno problema local. Você lida com aqueles que antecipa e, se algo inesperado acontecer, você deixa para os manipuladores de nível superior, em vez de engolir. Se você não se importa com uma exceção que você não esperava, você aposta alguém na pilha de chamadas e seria sabotagem eliminar uma exceção antes que ela atinja seu manipulador.

"Mas ... mas ... então uma dessas outras exceções com as quais eu não me importo pode fazer com que minha tarefa falhe!"

Sim. Mas eles sempre serão mais importantes que os gerenciados localmente. Como um alarme de incêndio ou o chefe dizendo para você parar o que está fazendo e pegar uma tarefa mais urgente que apareceu.

Martin Maat
fonte
-3

Você deve capturar exceções gerais no nível superior de cada processo e tratá-lo relatando o bug da melhor maneira possível e finalizando o processo.

Você não deve capturar exceções gerais e tentar continuar a execução.

ddyer
fonte