Por que não devo agrupar todos os blocos em "try" - "catch"?


430

Sempre acreditei que, se um método pode gerar uma exceção, é imprudente não proteger essa chamada com um bloqueio de tentativa significativo.

Acabei de publicar ' Você SEMPRE deve atender chamadas que podem tentar, pegar blocos. 'a essa pergunta e disseram que era' um conselho notavelmente ruim '- eu gostaria de entender o porquê.


4
Se eu soubesse que um método lançaria uma exceção, eu o teria corrigido em primeiro lugar. É porque eu não sei onde e por que o código lançará exceções que eu as pego na fonte (cada método - uma tentativa genérica de captura). Por exemplo, uma equipe me deu uma base de código com um banco de dados anterior. Muitas colunas estavam ausentes e capturadas somente após a adição de try catch na camada SQL. Em segundo lugar, posso registrar o nome e a mensagem do método, para depuração offline; caso contrário, não sei de onde ele se originou.
Chakra

Respostas:


340

Um método só deve capturar uma exceção quando pode lidar com isso de alguma maneira sensata.

Caso contrário, passe adiante, na esperança de que um método acima da pilha de chamadas possa fazer sentido.

Como outros observaram, é uma boa prática ter um manipulador de exceções sem tratamento (com log) no nível mais alto da pilha de chamadas para garantir que quaisquer erros fatais sejam registrados.


12
Também é importante notar que existem custos (em termos de código gerado) para tryblocos. Há uma boa discussão no "C ++ mais eficaz" de Scott Meyers.
Nick Meyer #

28
Na verdade, os tryblocos são livres em qualquer compilador C moderno; essas informações são datadas de Nick. Também não concordo em ter um manipulador de exceção de nível superior porque você perde as informações da localidade (o local real onde a instrução falhou).
Blindy

31
@ Cegamente: o manipulador de exceções não está lá para lidar com a exceção, mas na verdade gritar em voz alta que houve uma exceção não tratada, transmitir sua mensagem e encerrar o programa de maneira graciosa (retorne 1 em vez de chamar terminate) . É mais um mecanismo de segurança. Além disso, try/catchsão mais ou menos gratuitos quando não há nenhuma exceção. Quando há uma propagação, ela consome tempo cada vez que é lançada e capturada; portanto, uma cadeia try/catchdesse único relançamento não custa nada.
Matthieu M.

17
Eu discordo que você sempre deve colidir com uma exceção não capturada. O design moderno do software é muito compartimentado; então, por que você deveria punir o restante do aplicativo (e mais importante, o usuário!) Só porque houve um erro? Falhando na última coisa que você deseja fazer, tente ao menos fornecer ao usuário uma pequena janela de código que permita salvar o trabalho, mesmo que o restante do aplicativo não possa ser acessado.
Kendall Helmstetter Gelner

21
Kendall: se uma exceção chegar a um manipulador de nível superior, seu aplicativo estará por definição em um estado indefinido. Embora em alguns casos específicos possa ser útil preservar os dados do usuário (a recuperação de documentos do Word vem à mente), o programa não deve sobrescrever nenhum arquivo ou se comprometer com um banco de dados.
amigos estão dizendo sobre hugh

136

Como Mitch e outros declararam, você não deve capturar uma exceção que não planeja manipular de alguma forma. Você deve considerar como o aplicativo manipulará sistematicamente as exceções ao projetá-lo. Isso geralmente leva a ter camadas de tratamento de erros com base nas abstrações - por exemplo, você lida com todos os erros relacionados ao SQL em seu código de acesso a dados para que a parte do aplicativo que está interagindo com os objetos do domínio não seja exposta ao fato de que é um DB sob o capô em algum lugar.

Existem alguns odores de código relacionados que você definitivamente deseja evitar, além do cheiro de "pegar tudo em qualquer lugar" .

  1. "catch, log, rehrow" : se você deseja log baseado em escopo, escreva uma classe que emita uma instrução de log em seu destruidor quando a pilha estiver desenrolando devido a uma exceção (ala std::uncaught_exception()). Tudo o que você precisa fazer é declarar uma instância de log no escopo em que você está interessado e, voila, você possui log e nenhuma lógica try/ desnecessária catch.

  2. "pegar, jogar traduzido" : isso geralmente aponta para um problema de abstração. A menos que esteja implementando uma solução federada na qual esteja traduzindo várias exceções específicas em mais uma genérica, você provavelmente terá uma camada desnecessária de abstração ... e não diga "Talvez eu precise disso amanhã" .

  3. "pegar, limpar, relançar" : esse é um dos meus ódios de estimação. Se você perceber muito disso, aplique técnicas de Aquisição de recursos e inicialização e coloque a parte de limpeza no destruidor de uma instância de objeto de zelador .

Considero que o código repleto de try/ catchblocks é um bom alvo para a revisão e refatoração de código. Indica que o tratamento de exceções não é bem compreendido ou o código se tornou um amœba e está com séria necessidade de refatoração.


6
# 1 é novo para mim. +1 para isso. Além disso, gostaria de observar uma exceção comum ao item 2, que é o seguinte: se você estiver projetando uma biblioteca, convém converter exceções internas em algo especificado pela interface da sua biblioteca para reduzir o acoplamento (pode ser o que você quer dizer por "solução federada", mas não estou familiarizado com esse termo).
Rmeador 29/04


1
O número 2, onde não é um cheiro de código, mas faz sentido, pode ser aprimorado mantendo a exceção antiga como aninhada.
Deduplicator

1
Em relação ao item 1: std :: uncaught_exception () diz que há uma exceção não capturada em voo, mas o AFAIK apenas uma cláusula catch () permite determinar o que realmente é essa exceção. Portanto, enquanto você pode registrar o fato de que está saindo de um escopo devido a uma exceção não capturada, apenas uma tentativa / captura anexa permite registrar todos os detalhes. Corrigir?
Jeremy

@ Jeremy - você está correto. Normalmente, registro os detalhes da exceção quando lida com a exceção. Ter um rastreamento dos quadros intermediários é muito útil. Você geralmente precisa registrar o identificador de encadeamento ou algum contexto de identificação, para correlacionar as linhas de log. Usei uma Loggerclasse semelhante à log4j.Loggerque inclui o ID do thread em todas as linhas de log e emiti um aviso no destruidor quando uma exceção estava ativa.
precisa saber é o seguinte

48

Como a próxima pergunta é "Eu peguei uma exceção, o que devo fazer a seguir?" O que você vai fazer? Se você não fizer nada - isso é ocultação de erros e o programa "simplesmente não funciona" sem chance de descobrir o que aconteceu. Você precisa entender o que exatamente fará depois de capturar a exceção e capturar apenas se souber.


29

Você não precisa cobrir todos os blocos com try-catchs porque um try-catch ainda pode capturar exceções sem tratamento lançadas em funções mais abaixo na pilha de chamadas. Portanto, em vez de todas as funções terem um try-catch, você pode ter uma na lógica de nível superior do seu aplicativo. Por exemplo, pode haver uma SaveDocument()rotina de nível superior, que chama muitos métodos que chamam outros métodos etc. Esses sub-métodos não precisam de suas próprias tentativas de captura, porque se lançarem, ainda serão pegos pela SaveDocument()captura.

Isso é bom por três razões: é útil porque você tem um único local para relatar um erro: o SaveDocument()(s) bloco (s) de captura. Não há necessidade de repetir isso em todos os sub-métodos, e é o que você deseja: um único local para fornecer ao usuário um diagnóstico útil sobre algo que deu errado.

Segundo, o salvamento é cancelado sempre que uma exceção é lançada. Com cada sub-método de tentativa de captura, se uma exceção é lançada, você começa no bloco catch desse método, as folhas de execução da função, e continua através SaveDocument(). Se algo já deu errado, você provavelmente quer parar por aí.

Terceiro, todos os seus sub-métodos podem assumir que todas as chamadas são bem-sucedidas . Se uma chamada falhar, a execução passará para o bloco catch e o código subsequente nunca será executado. Isso pode tornar seu código muito mais limpo. Por exemplo, aqui está o código de erro:

int ret = SaveFirstSection();

if (ret == FAILED)
{
    /* some diagnostic */
    return;
}

ret = SaveSecondSection();

if (ret == FAILED)
{
    /* some diagnostic */
    return;
}

ret = SaveThirdSection();

if (ret == FAILED)
{
    /* some diagnostic */
    return;
}

Veja como isso pode ser escrito com exceções:

// these throw if failed, caught in SaveDocument's catch
SaveFirstSection();
SaveSecondSection();
SaveThirdSection();

Agora está muito mais claro o que está acontecendo.

Observe que o código seguro de exceção pode ser mais difícil de escrever de outras maneiras: você não deseja vazar memória se uma exceção for lançada. Verifique se você conhece RAII , contêineres STL, ponteiros inteligentes e outros objetos que liberam seus recursos em destruidores, pois os objetos são sempre destruídos antes das exceções.


2
Exemplos esplêndidos. Sim, pegue o mais alto possível, em unidades lógicas, como em torno de alguma operação 'transacional' como um load / save / etc. Nada parece pior do que o código salpicado com repetitivo e redundante try- catchblocos que tentativa de flag-se todas as permutações ligeiramente diferente de algum erro com uma mensagem um pouco diferente, quando na realidade todos eles devem terminar a mesma: transação ou programa de fracasso e saída! Se ocorrer uma falha digna de exceção, aposto que a maioria dos usuários só quer recuperar o que pode ou, pelo menos, ficar sozinha sem precisar lidar com 10 níveis de mensagem sobre isso.
underscore_d

Só queria dizer que esta é uma das melhores explicações do tipo "jogue cedo, pegue tarde" que já li: conciso e os exemplos ilustram perfeitamente seus pontos de vista. Obrigado!
precisa saber é o seguinte

27

Herb Sutter escreveu sobre este problema aqui . Com certeza vale a pena ler.
Um teaser:

"Escrever código com exceção de segurança é fundamental sobre escrever 'try' e 'catch' nos lugares corretos." Discutir.

Em outras palavras, essa afirmação reflete um mal-entendido fundamental sobre segurança de exceção. As exceções são apenas outra forma de relatório de erros, e certamente sabemos que escrever código com segurança não é apenas para verificar códigos de retorno e lidar com condições de erro.

Na verdade, verifica-se que a segurança das exceções raramente é sobre escrever 'tentar' e 'pegar' - e quanto mais raramente, melhor. Além disso, nunca se esqueça que a segurança de exceção afeta o design de um código; nunca é apenas uma reflexão tardia que pode ser adaptada com algumas declarações de captura extras como se fosse tempero.


15

Conforme declarado em outras respostas, você deve capturar uma exceção apenas se puder fazer algum tipo de tratamento sensível a erros.

Por exemplo, na pergunta que gerou sua pergunta, o questionador pergunta se é seguro ignorar exceções para a lexical_castde um número inteiro para uma sequência. Tal elenco nunca deve falhar. Se falhasse, algo daria terrivelmente errado no programa. O que você poderia fazer para se recuperar nessa situação? Provavelmente é melhor deixar o programa morrer, pois está em um estado que não pode ser confiável. Portanto, não lidar com a exceção pode ser a coisa mais segura a se fazer.


12

Se você sempre manipula exceções imediatamente no chamador de um método que pode gerar uma exceção, as exceções se tornam inúteis e é melhor usar códigos de erro.

O ponto principal das exceções é que elas não precisam ser tratadas em todos os métodos da cadeia de chamadas.


9

O melhor conselho que ouvi é que você deve sempre capturar exceções em pontos onde possa sensatamente fazer algo sobre a condição excepcional e que "capturar, registrar e liberar" não é uma boa estratégia (se ocasionalmente inevitável nas bibliotecas).


2
@ KeithB: Considero a segunda melhor estratégia. É melhor se você conseguir escrever o log de outra maneira.
precisa

1
@ KeithB: É uma estratégia "melhor que nada em uma biblioteca". "Capturar, registrar, lidar com isso adequadamente" é melhor sempre que possível. (Sim, eu sei que nem sempre é possível.)
Donal Fellows

6

Concordo com a orientação básica da sua pergunta para lidar com tantas exceções quanto possível no nível mais baixo.

Algumas das respostas existentes são do tipo "Você não precisa lidar com a exceção. Outra pessoa fará isso na pilha". Para minha experiência, isso é uma desculpa ruim para não pensar no tratamento de exceções no pedaço de código desenvolvido no momento, fazendo com que o tratamento de exceções seja o problema de outra pessoa ou posteriormente.

Esse problema cresce dramaticamente no desenvolvimento distribuído, onde você pode precisar chamar um método implementado por um colega de trabalho. E então você deve inspecionar uma cadeia aninhada de chamadas de método para descobrir por que ele / ela está lançando alguma exceção para você, que poderia ter sido tratada com muito mais facilidade no método aninhado mais profundo.


5

O conselho que meu professor de ciência da computação me deu uma vez foi: "Use os blocos Try and Catch apenas quando não for possível lidar com o erro usando meios padrão".

Como exemplo, ele nos disse que se um programa tivesse algum problema sério em um local onde não seria possível fazer algo como:

int f()
{
    // Do stuff

    if (condition == false)
        return -1;
    return 0;
}

int condition = f();

if (f != 0)
{
    // handle error
}

Então você deve usar try, catch blocks. Embora você possa usar exceções para lidar com isso, geralmente não é recomendado porque as exceções são caras em termos de desempenho.


7
Essa é uma estratégia, mas muitas pessoas recomendam nunca retornar códigos de erro ou status de falha / sucesso das funções, usando exceções. O tratamento de erros baseado em exceções é geralmente mais fácil de ler do que o código baseado em código de erro. (Veja a resposta da AshleysBrain a esta pergunta para um exemplo.) Além disso, lembre-se sempre de que muitos professores de ciência da computação têm muito pouca experiência em escrever código real.
21810 Kristoff Johnson

1
-1 @Sagelika Sua resposta consiste em evitar exceções, portanto, não há necessidade de tentar pegar.
Vicente Botet Escriba

3
@ Christopher: Outras grandes desvantagens do código de retorno é que é muito fácil esquecer de verificar um código de retorno e logo após a ligação não é necessariamente o melhor lugar para lidar com o problema.
precisa

depende, mas em muitos casos (deixando de lado as pessoas que jogam quando realmente não deveriam), as exceções são superiores aos códigos de retorno por muitos motivos. na maioria dos casos, a ideia de que exceções são prejudiciais ao desempenho é um grande ol'[carece de fontes?]
underscore_d

3

Se você deseja testar o resultado de todas as funções, use códigos de retorno.

O objetivo das exceções é para que você possa testar os resultados com menos frequência. A idéia é separar condições excepcionais (incomuns, mais raras) do seu código mais comum. Isso mantém o código comum mais limpo e simples - mas ainda é capaz de lidar com essas condições excepcionais.

No código bem projetado, funções mais profundas podem ser lançadas e funções mais altas podem ser detectadas. Mas a chave é que muitas funções "intermediárias" estarão livres do ônus de lidar com condições excepcionais. Eles só precisam ser "protegidos contra exceções", o que não significa que devam pegar.


2

Gostaria de acrescentar a esta discussão que, desde C ++ 11 , faz muito sentido, desde que cada catchbloco seja rethrowuma exceção até o ponto em que possa / deva ser tratado. Dessa forma, um backtrace pode ser gerado . Portanto, acredito que as opiniões anteriores estão em parte desatualizadas.

Use std::nested_exceptionestd::throw_with_nested

É descrito no StackOverflow aqui e aqui como conseguir isso.

Como você pode fazer isso com qualquer classe de exceção derivada, você pode adicionar muitas informações a esse backtrace! Você também pode dar uma olhada no meu MWE no GitHub , onde um backtrace seria algo assim:

Library API: Exception caught in function 'api_function'
Backtrace:
~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt"

2

Tive a "oportunidade" de salvar vários projetos e os executivos substituíram toda a equipe de desenvolvimento porque o aplicativo apresentava muitos erros e os usuários estavam cansados ​​dos problemas e contornos. Todas essas bases de código tinham tratamento centralizado de erros no nível do aplicativo, como a resposta mais votada descreve. Se essa resposta é a melhor prática, por que não funcionou e permitiu que a equipe de desenvolvimento anterior resolvesse problemas? Talvez às vezes não funcione? As respostas acima não mencionam quanto tempo os desenvolvedores passam consertando problemas únicos. Se o tempo para resolver problemas é a métrica principal, o código de instrumentação com blocos try..catch é uma prática recomendada.

Como minha equipe resolveu os problemas sem alterar significativamente a interface do usuário? Simples, todos os métodos foram instrumentados com o try..catch bloqueado e tudo foi registrado no ponto de falha com o nome do método, os valores dos parâmetros do método concatenados em uma sequência passada juntamente com a mensagem de erro, a mensagem de erro, o nome do aplicativo, a data, e versão. Com essas informações, os desenvolvedores podem executar análises sobre os erros para identificar a exceção que mais ocorre! Ou o espaço para nome com o maior número de erros. Também pode validar que um erro que ocorre em um módulo seja tratado adequadamente e não seja causado por vários motivos.

Outro benefício profissional disso é que os desenvolvedores podem definir um ponto de interrupção no método de registro de erros e, com um ponto de interrupção e um único clique no botão de depuração "sair", eles estão no método que falhou com acesso total ao objetos no ponto de falha, convenientemente disponíveis na janela imediata. Isso facilita a depuração e permite arrastar a execução de volta ao início do método para duplicar o problema e encontrar a linha exata. O tratamento centralizado de exceções permite que um desenvolvedor replique uma exceção em 30 segundos? Não.

A declaração "Um método só deve capturar uma exceção quando pode lidar com isso de alguma maneira sensata". Isso implica que os desenvolvedores podem prever ou encontrarão todos os erros que podem ocorrer antes do lançamento. Se isso fosse verdade, o manipulador de exceções de aplicativos não seria necessário e não haveria mercado para Elastic Search e logstash.

Essa abordagem também permite que os desenvolvedores encontrem e corrijam problemas intermitentes na produção! Deseja depurar sem um depurador na produção? Ou você prefere atender e receber e-mails de usuários chateados? Isso permite que você corrija problemas antes que alguém saiba e sem precisar enviar e-mail, mensagens instantâneas ou Slack com suporte, pois tudo o que é necessário para corrigir o problema está aqui. 95% dos problemas nunca precisam ser reproduzidos.

Para funcionar corretamente, ele precisa ser combinado com o log centralizado que pode capturar o espaço para nome / módulo, nome da classe, método, entradas e mensagem de erro e armazenar em um banco de dados para que possa ser agregado para destacar qual método falha mais, para que possa ser corrigido primeiro.

Às vezes, os desenvolvedores escolhem lançar exceções na pilha a partir de um bloco catch, mas essa abordagem é 100 vezes mais lenta que o código normal que não gera. Captura e liberação com registro é o preferido.

Essa técnica foi usada para estabilizar rapidamente um aplicativo que falhou a cada hora para a maioria dos usuários em uma empresa da Fortune 500 desenvolvida por 12 desenvolvedores ao longo de 2 anos. Usando essas 3000 exceções diferentes, foram identificadas, corrigidas, testadas e implantadas em 4 meses. A média é de uma correção a cada 15 minutos, em média, por 4 meses.

Concordo que não é divertido digitar tudo o que é necessário para instrumentar o código e prefiro não olhar para o código repetitivo, mas adicionar 4 linhas de código a cada método vale a pena a longo prazo.


1
Embrulhar cada bloco parece exagero. Ele rapidamente torna seu código inchado e doloroso de ler. Registrar um rastreamento de pilha de uma exceção em níveis mais altos mostra onde o problema ocorreu e que combinado com o erro em si geralmente são informações suficientes para continuar. Gostaria de saber onde você achou isso insuficiente. Só para ganhar a experiência de outra pessoa.
user441521

1
"As exceções são 100 a 1000 vezes mais lentas que o código normal e nunca devem ser mostradas novamente" - essa afirmação não é verdadeira na maioria dos compiladores e hardwares modernos.
Mitch Wheat

Parece um exagero e requer um pouco de digitação, mas é a única maneira de executar análises em exceções para encontrar e corrigir os maiores erros primeiro, incluindo erros intermitentes na produção. O bloco catch manipula erros específicos, se necessário, e possui uma única linha de código que é registrada.
user2502917

Não, as exceções são muito lentas. A alternativa são códigos de retorno, objetos ou variáveis. Veja este post de estouro de pilha ... "as exceções são pelo menos 30.000 vezes mais lentas que os códigos de retorno" stackoverflow.com/questions/891217/…
user2502917

1

Além do conselho acima, pessoalmente eu uso alguns try + catch + throw; pelo seguinte motivo:

  1. No limite do codificador diferente, eu uso try + catch + throw no código escrito por mim, antes que a exceção seja lançada para o chamador que é escrito por outras pessoas, isso me dá a chance de conhecer alguma condição de erro ocorrida no meu código e esse lugar é muito mais próximo do código que inicialmente lança a exceção, quanto mais próximo, mais fácil é encontrar o motivo.
  2. No limite dos módulos, embora um módulo diferente possa ser escrito da mesma pessoa.
  3. Com o objetivo de Aprender + Depurar, neste caso eu uso catch (...) em C ++ e catch (Exception ex) em C #, para C ++, a biblioteca padrão não lança muitas exceções, portanto, este caso é raro em C ++. Mas o lugar comum em C #, C # tem uma biblioteca enorme e uma hierarquia madura de exceções, o código da biblioteca C # gera muitas exceções, na teoria eu (e você) devemos conhecer todas as exceções da função que você chamou e saber o motivo / o motivo. essas exceções são lançadas e sabem como lidar com elas (passar ou capturar e manipular no lugar) normalmente. Infelizmente, na realidade, é muito difícil saber tudo sobre as possíveis exceções antes de escrever uma linha de código. Então, eu pego tudo e deixo meu código falar alto registrando (no ambiente do produto) / afirmo a caixa de diálogo (no ambiente de desenvolvimento) quando qualquer exceção realmente ocorre. Dessa forma, adiciono código de manipulação de exceção progressivamente. Sei que conflita com bons conselhos, mas, na realidade, funciona para mim e não conheço nenhuma maneira melhor para esse problema.

1

Sinto-me compelido a acrescentar outra resposta, embora a resposta de Mike Wheat resuma os pontos principais muito bem. Eu penso assim. Quando você tem métodos que fazem várias coisas, você está multiplicando a complexidade, não a adicionando.

Em outras palavras, um método envolvido em uma tentativa de captura tem dois resultados possíveis. Você tem o resultado de não exceção e o resultado de exceção. Quando você lida com muitos métodos, isso explode exponencialmente além da compreensão.

Exponencialmente, porque se cada método se ramifica de duas maneiras diferentes, toda vez que você chama outro método, está elevando o número anterior de possíveis resultados. Quando você chama cinco métodos, você tem até 256 resultados possíveis, no mínimo. Compare isto a não fazer um try / catch em cada método único e você só tem um caminho a seguir.

É basicamente assim que eu olho para isso. Você pode ficar tentado a argumentar que qualquer tipo de ramificação faz a mesma coisa, mas as tentativas / capturas são um caso especial porque o estado do aplicativo basicamente se torna indefinido.

Então, resumindo, try / catch torna o código muito mais difícil de entender.


-2

Você não precisa cobrir todas as partes do seu código try-catch. O principal uso do try-catchbloco é o tratamento de erros e possui bugs / exceções no seu programa. Algum uso de try-catch-

  1. Você pode usar esse bloco onde deseja manipular uma exceção ou simplesmente pode dizer que o bloco de código escrito pode gerar uma exceção.
  2. Se você deseja descartar seus objetos imediatamente após o uso, use o try-catchbloco.

1
"Se você deseja descartar seus objetos imediatamente após o uso deles, poderá usar o bloco try-catch". Você pretendia promover RAII / vida útil mínima do objeto? Nesse caso, bem, try/ catché completamente separado / ortogonal disso. Se você deseja dispor objetos em um escopo menor, basta abrir um novo { Block likeThis; /* <- that object is destroyed here -> */ }- não é necessário agrupar isso try/ a catchmenos que você realmente precise de catchalguma coisa, é claro.
underscore_d

# 2 - Descartar objetos (que foram criados manualmente) na exceção parece estranho para mim, isso pode ser útil em alguns idiomas, sem dúvida, mas geralmente você faz isso em uma tentativa / finalmente "dentro do bloco tentar / exceto", e não especificamente no próprio bloco de exceção - já que o objeto em si pode ter sido a causa da exceção em primeiro lugar e, portanto, causar outra exceção e potencialmente uma falha.
TS
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.