Argumentos contra a supressão de erros


35

Encontrei um código como este em um de nossos projetos:

    SomeClass QueryServer(string args)
    {
        try
        {
            return SomeClass.Parse(_server.Query(args));
        }
        catch (Exception)
        {
            return null;
        }
    }

Tanto quanto eu entendo, suprimir erros como esse é uma prática ruim, pois destrói informações úteis da exceção do servidor original e faz com que o código continue quando realmente deve terminar.

Quando é apropriado suprimir completamente todos os erros como este?


13
Eric Lippert escreveu um excelente post no blog chamado vexing exceptions, onde classifica as exceções em quatro categorias diferentes e sugere qual é a abordagem correta para cada uma. Escrito a partir de uma perspectiva C #, mas aplicável entre idiomas, acredito.
23416 Damien_The_Unbeliever

3
Eu acredito que o código viola o princípio "Fail-fast". Há uma alta probabilidade de que o bug real esteja oculto nele.
eufórico


4
Você está pegando muito. Você também estaria capturando bugs, como NRE, índice de array fora dos limites, erro de configuração, ... Isso impede que você encontre esses bugs e eles causarão danos continuamente.
usr

Respostas:


52

Imagine o código com milhares de arquivos usando várias bibliotecas. Imagine que todos eles são codificados assim.

Imagine, por exemplo, que uma atualização do seu servidor faça com que um arquivo de configuração desapareça; e agora tudo o que você tem é um rastreamento de pilha é uma exceção de ponteiro nulo ao tentar usar essa classe: como você resolveria isso? Pode levar horas, onde pelo menos apenas o registro do rastreamento de pilha bruta do arquivo não encontrado [caminho do arquivo] pode permitir que você resolva momentaneamente.

Ou ainda pior: uma falha em uma das bibliotecas que você usa após uma atualização que faz com que seu código falhe mais tarde. Como você pode rastrear isso de volta à biblioteca?

Mesmo sem um tratamento robusto de erros, apenas fazendo

throw new IllegalStateException("THIS SHOULD NOT HAPPENING")

ou

LOGGER.error("[method name]/[arguments] should not be there")

pode economizar horas de tempo.

Mas há alguns casos em que você pode realmente querer ignorar a exceção e retornar nulo como este (ou não fazer nada). Especialmente se você se integrar a algum código legado mal projetado e essa exceção for esperada como um caso normal.

De fato, ao fazer isso, você deve se perguntar se realmente está ignorando a exceção ou "lidando adequadamente" com suas necessidades. Se retornar null estiver "manipulando adequadamente" sua exceção nesse caso, faça-o. E adicione um comentário por que isso é a coisa certa a se fazer.

As melhores práticas são coisas a serem seguidas na maioria dos casos, talvez 80%, talvez 99%, mas você sempre encontrará um caso de ponta em que elas não se aplicam. Nesse caso, deixe um comentário por que você não está seguindo a prática para os outros (ou mesmo para você) que lerão seu código meses depois.


7
+1 para explicar por que você acha que precisa fazer isso em um comentário. Sem uma explicação, a deglutição de exceção sempre provocava alarme na minha cabeça.
Jpmc26

Meu problema com isso é que o comentário está preso dentro desse código. Como consumidor da função, talvez eu nunca consiga ver esse comentário.
CorsiKa

@ jpmc26 Se a exceção for tratada (por exemplo, retornando um valor especial), isso não significa que a exceção seja ingerida.
Deduplicator

2
@corsiKa um consumidor não precisa conhecer os detalhes da implementação se a função estiver fazendo seu trabalho corretamente. talvez eles façam parte disso nas bibliotecas apache ou na primavera e você não saiba disso.
Walfrat 24/02

@Deduplicator sim, mas usando uma captura como parte de seu algoritmo não é suposto ser um bom pratice também :)
Walfrat

14

Há casos em que esse padrão é útil - mas eles geralmente são usados ​​quando a exceção gerada nunca deveria ter sido (ou seja, quando as exceções são usadas para o comportamento normal).

Por exemplo, imagine que você tenha uma classe que abre um arquivo, armazena-o no objeto retornado. Se o arquivo não existir, considere que não é um caso de erro, você retorna nulo e permite que o usuário crie um novo arquivo. O método de abertura de arquivo pode lançar uma exceção aqui para indicar a ausência de arquivo, portanto, capturá-lo silenciosamente pode ser uma situação válida.

No entanto, não é muito comum você querer fazer isso e, se o código estiver repleto de um padrão desses, você deve lidar com isso agora. No mínimo, eu esperaria que uma captura tão silenciosa escrevesse uma linha de log dizendo que foi isso que aconteceu (você tem rastreamento, certo) e um comentário para explicar esse comportamento.


2
"usado quando a exceção gerada nunca deveria ter sido" tal coisa. O ponto de exceção é sinalizar que algo deu terrivelmente errado.
eufórico

3
@ Eufórico Mas às vezes o escritor de uma biblioteca não conhece as intenções do usuário final dessa biblioteca. A pessoa "terrivelmente errada" pode ser a condição facilmente recuperável de outra pessoa.
Simon B

2
@ Eufórico, depende do que sua linguagem considera "terrivelmente errado": o pessoal do Python gosta de exceções para coisas que estão muito próximas do controle de fluxo em (por exemplo,) C ++. Retornar nulo pode ser defensável em algumas situações - por exemplo, leitura de um cache em que itens podem ser repentinamente e imprevisivelmente despejados.
Matt Krause

10
@Euphoric, "O arquivo não encontrado não é uma exceção normal. Você deve primeiro verificar se o arquivo existe, se houver possibilidade de que ele não exista." Não! Não! Não! Não! Esse é o clássico pensamento errado sobre operações atômicas. O arquivo pode ser excluído entre você, verificando se ele existe e acessando-o. A única maneira correta de lidar com essas situações é acessá-las e lidar com as exceções, se ocorrerem, por exemplo, retornando nullse esse for o melhor que o idioma escolhido pode oferecer.
David Arno

3
@Euphoric: Então, escreva o código para lidar com a impossibilidade de acessar o arquivo pelo menos duas vezes? Isso viola o DRY, e também o código não exercido é código quebrado.
Deduplicator 23/02

7

Isso é 100% dependente do contexto. Se o chamador dessa função tiver o requisito de exibir ou registrar erros em algum lugar, é óbvio que isso não faz sentido. Se o chamador ignorasse todos os erros ou mensagens de exceção, não faria muito sentido retornar a exceção ou sua mensagem. Quando o requisito é fazer com que o código termine apenas sem exibir nenhum motivo, uma chamada

   if(QueryServer(args)==null)
      TerminateProgram();

provavelmente seria suficiente. Você deve considerar se isso tornará difícil encontrar a causa do erro pelo usuário - se for esse o caso, essa é a forma incorreta de tratamento de erros. No entanto, se o código de chamada se parecer com isso

  result = QueryServer(args);
  if(result!=null)
      DoSomethingMeaningful(result);
  // else ??? Mask the error and let the user run into trouble later

você deve discutir com o desenvolvedor durante a revisão do código. Se alguém implementa essa forma de tratamento sem erros apenas porque é muito preguiçoso para descobrir os requisitos corretos, esse código não deve entrar em produção e o autor do código deve ser ensinado sobre as possíveis conseqüências dessa preguiça. .


5

Nos anos que passei programando e desenvolvendo sistemas, há apenas duas situações em que achei o padrão em questão útil (em ambos os casos, a supressão continha também o registro da exceção lançada, não considero captura e nullretorno simples como uma boa prática. )

As duas situações são as seguintes:

1. Quando a exceção não foi considerada um estado excepcional

É quando você executa uma operação em alguns dados, que podem ser lançados, você sabe que podem ser lançados, mas você ainda deseja que seu aplicativo continue em execução, porque você não precisa dos dados processados. Se você os receber, é bom; se não, também é bom.

Alguns atributos opcionais de uma classe podem ser lembrados.

2. Ao fornecer uma nova (melhor, mais rápida?) Implementação de uma biblioteca usando uma interface já usada em um aplicativo

Imagine que você tenha um aplicativo usando algum tipo de biblioteca antiga, que não emitiu exceções, mas retornou nullpor erro. Então, você criou um adaptador para esta biblioteca, copiando praticamente a API original da biblioteca e está usando essa nova interface (ainda não lançada) em seu aplicativo e manipulando as nullverificações por conta própria.

É apresentada uma nova versão da biblioteca, ou talvez uma biblioteca completamente diferente, oferecendo a mesma funcionalidade que, em vez de retornar nulls, gera exceções e você deseja usá-la.

Você não deseja vazar as exceções para o aplicativo principal, portanto, suprimi-las e registrá-las no adaptador criado para quebrar essa nova dependência.


O primeiro caso não é um problema, é o comportamento desejado do código. Na segunda situação, no entanto, se em todo lugar o nullvalor de retorno do adaptador da biblioteca realmente significa um erro, refatorar a API para gerar uma exceção e capturá-la em vez de procurar nullpode ser (e geralmente é em termos de código) uma boa idéia.

Pessoalmente, uso a supressão de exceção apenas no primeiro caso. Eu o usei apenas para o segundo caso, quando não tínhamos orçamento para fazer o restante do aplicativo funcionar com as exceções em vez de nulls.


-1

Embora pareça lógico dizer que os programas só devem capturar exceções que eles sabem como manipular e não podem saber como lidar com exceções que não foram antecipadas pelo programador, essa afirmação ignora o fato de que muitas operações podem falhar em um ambiente. número quase ilimitado de maneiras que não têm efeitos colaterais e que, em muitos casos, o manuseio adequado para a grande maioria dessas falhas será idêntico; os detalhes exatos da falha serão irrelevantes e, consequentemente, não importa se o programador os antecipou.

Se, por exemplo, o objetivo de uma função é ler um arquivo de documento em um objeto adequado e criar uma nova janela de documento para mostrar esse objeto ou relatar ao usuário que o arquivo não pôde ser lido, uma tentativa de carregar um arquivo arquivo de documento inválido não deve travar o aplicativo - ele deve mostrar uma mensagem indicando o problema, mas deixar o restante do aplicativo continuar sendo executado normalmente, a menos que, por algum motivo, a tentativa de carregar o documento tenha corrompido o estado de outra coisa no sistema .

Fundamentalmente, o tratamento adequado de uma exceção geralmente depende menos do tipo de exceção do que do local em que foi lançada; se um recurso é protegido por um bloqueio de leitura e gravação e uma exceção é lançada dentro de um método que adquiriu o bloqueio para leitura, o comportamento adequado geralmente deve ser liberar o bloqueio, pois o método não pode ter feito nada com o recurso . Se uma exceção for lançada enquanto o bloqueio for adquirido para gravação, o bloqueio deve ser frequentemente invalidado, pois o recurso protegido pode estar em um estado inválido; se a primitiva de bloqueio não tiver um estado "inválido", adicione um sinalizador para rastrear essa invalidação. Liberar o bloqueio sem invalidá-lo é ruim porque outro código pode ver o objeto protegido em um estado inválido. Deixar a trava pendurada, no entanto, não é ' uma solução adequada também. A solução certa é invalidar o bloqueio para que quaisquer tentativas pendentes ou futuras de aquisição falhem imediatamente.

Se um recurso inválido for abandonado antes de tentar utilizá-lo, não há motivo para interromper o aplicativo. Se um recurso invalidado for crítico para a operação continuada de um aplicativo, o aplicativo precisará ser desativado, mas a invalidação do recurso provavelmente fará com que isso aconteça. O código que recebeu a exceção original geralmente não tem como saber qual situação se aplica, mas se invalida o recurso, pode garantir que a ação correta acabe sendo executada nos dois casos.


2
Isso não responde à pergunta porque manipula uma exceção sem conhecer os detalhes da exceção! = Consumindo silenciosamente uma exceção (genérica).
Taemyr 24/02

@ Taemyr: Se o objetivo de uma função é "fazer X se possível, ou então indicar que não foi", e X não foi possível, será impraticável listar todos os tipos de exceções que nem a função nem o chamador se preocupará além de "Não foi possível fazer o X". O manuseio de exceções de Pokemon geralmente é a única maneira prática de fazer as coisas funcionarem nesses casos, e não precisa ser tão ruim quanto algumas pessoas percebem se o código é disciplinado sobre a invalidação de coisas que são corrompidas por exceções inesperadas.
supercat 24/02

Exatamente. Todo método deve ter um objetivo, se ele pode cumprir seu objetivo, independentemente de algum método que ele chama lança uma exceção ou não, então engolir a exceção, seja lá o que for, e fazer seu trabalho, é a coisa certa a fazer. A manipulação de erros é fundamentalmente sobre a conclusão da tarefa após um erro. Isso é o que importa, não o erro que ocorreu.
jmoreno

@jmoreno: O que dificulta as coisas é que, em muitas situações, haverá um grande número de exceções, muitas das quais um programador não anteciparia particularmente, o que não indicaria um problema, e algumas exceções, algumas das quais um programador não Preveja particularmente, o que indica um problema, e não existe uma maneira sistemática definida para distingui-los. A única maneira que conheço de lidar com isso é ter exceções que invalidam coisas que foram corrompidas; portanto, se importam, são notadas e se não importam, são ignoradas.
supercat

-2

A ingestão desse tipo de erro não é particularmente útil para ninguém. Sim, o chamador original pode não se importar com a exceção, mas outra pessoa pode .

Então o que eles fazem? Eles adicionam código para lidar com a exceção e ver qual sabor de exceção está recebendo de volta. Ótimo. Mas se o manipulador de exceções for deixado em, o chamador original não será mais retornado nulo e algo será interrompido em outro lugar.

Mesmo se você estiver ciente de que algum código upstream pode gerar um erro e, assim, retornar nulo, seria seriamente inerte da sua parte não tentar pelo menos evitar a exceção no código de chamada IMHO.


"seriamente inerte" - você quis dizer "seriamente inepto"?
Nome de tela esotérico

@EsotericScreenName Escolha um ... ;-)
Robbie Dee

-3

Eu já vi exemplos em que bibliotecas de terceiros tinham métodos potencialmente úteis, exceto que lançam exceções em alguns casos que devem funcionar para mim. O que eu posso fazer?

  • Implementar exatamente o que eu preciso. Pode ser difícil em um prazo.
  • Modifique o código de terceiros. Mas então eu tenho que procurar conflitos de mesclagem a cada atualização.
  • Escreva um método de invólucro para transformar a exceção em um ponteiro nulo comum, em um falso booleano ou em qualquer outra coisa.

Por exemplo, o método da biblioteca

public foo findFoo() {...} 

retorna o primeiro foo, mas se não houver, ele lança uma exceção. Mas o que eu preciso é de um método para retornar o primeiro foo ou um nulo. Então eu escrevo este:

public foo myFindFoo() {
    try {
        return findFoo() 
    } 
    catch (NoFooException ex) {
        return null;
    }
}

Não é legal. Mas às vezes a solução pragmática.


11
O que você quer dizer com "lançar exceções onde elas devem funcionar para você"?
CorsiKa

@corsiKa, o exemplo que me vem à cabeça são métodos de pesquisa em uma lista ou estrutura de dados semelhante. Eles falham quando não encontram nada ou retornam um valor nulo? Outro exemplo são os métodos waitUntil que falham no tempo limite em vez de retornar um valor booleano falso.
om
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.