Como escrever uma boa mensagem de exceção

101

Atualmente, estou fazendo uma revisão de código e uma das coisas que noto são o número de exceções em que a mensagem de exceção parece reiterar onde ocorreu a exceção. por exemplo

throw new Exception("BulletListControl: CreateChildControls failed.");

Todos os três itens desta mensagem eu posso resolver do resto da exceção. Conheço a classe e o método do rastreamento de pilha e sei que falhou (porque tenho uma exceção).

Isso me fez pensar em qual mensagem eu coloquei em mensagens de exceção. Primeiro, crio uma classe de exceção, se ainda não existir, pelo motivo geral (por exemplo PropertyNotFoundException- o porquê ) e, quando a jogo, a mensagem indica o que deu errado (por exemplo, "Não é possível encontrar a propriedade 'IDontExist' no nó 1234 "- o que ). O onde está no StackTrace. O quando pode terminar no log (se aplicável). O como é para o desenvolvedor trabalhar (e corrigir)

Você tem outras dicas para lançar exceções? Especificamente no que diz respeito à criação de novos tipos e à mensagem de exceção.

Colin Mackay
fonte
4
São para arquivos de log ou para apresentar ao usuário?
Jon Hopkins
5
Apenas para depuração. Eles podem acabar em um log. Eles não seriam apresentados ao usuário. Não sou fã de apresentar mensagens de exceção ao usuário.
Colin Mackay

Respostas:

72

Vou direcionar minha resposta mais para o que vem depois de uma exceção: para que serve e como o software deve se comportar, o que seus usuários devem fazer com a exceção? Uma ótima técnica que me deparei no início de minha carreira foi sempre relatar problemas e erros em três partes: contexto, problema e solução. O uso dessa diciplina altera enormemente o tratamento de erros e torna o software muito melhor para os operadores usarem.

Aqui estão alguns exemplos.

Context: Saving connection pooling configuration changes to disk.
Problem: Write permission denied on file '/xxx/yyy'.
Solution: Grant write permission to the file.

Nesse caso, o operador sabe exatamente o que fazer e para qual arquivo deve ser afetado. Eles também sabem que as alterações no pool de conexões não foram necessárias e devem ser repetidas.

Context: Sending email to '[email protected]' regarding 'Blah'.
Problem: SMTP connection refused by server 'mail.xyz.com'.
Solution: Contact the mail server administrator to report a service problem.  The email will be sent later. You may want to tell '[email protected]' about this problem.

Eu escrevo sistemas do lado do servidor e meus operadores geralmente oferecem suporte técnico de primeira linha. Eu escreveria as mensagens de maneira diferente para o software de desktop que tem um público diferente, mas inclui as mesmas informações.

Várias coisas maravilhosas acontecem se alguém usa essa técnica. O desenvolvedor de software geralmente está em melhor posição para saber como resolver os problemas em seu próprio código, portanto, codificar soluções dessa maneira ao escrever o código é de grande benefício para os usuários finais que estão em desvantagem em encontrar soluções, pois geralmente faltam informações sobre o que exatamente o software estava fazendo. Quem já leu uma mensagem de erro do Oracle saberá o que quero dizer.

A segunda coisa maravilhosa que vem à mente é quando você se encontra tentando descrever uma solução em sua exceção e está escrevendo "Marque X e se A então B mais C". Este é um sinal muito claro e óbvio de que sua exceção está sendo verificada no lugar errado. Você programador tem a capacidade de comparar as coisas no código, de modo que as instruções "if" devem ser executadas no código, por que envolver o usuário em algo que pode ser automatizado? Provavelmente, isso é mais profundo no código e alguém fez a coisa preguiçosa e lançou o IOException de vários métodos e detectou possíveis erros de todos eles em um bloco de código de chamada que não pode descrever adequadamente o que deu errado, o que o específicocontexto é e como corrigi-lo. Isso o encoraja a escrever erros de granulação mais refinados, capturá-los e manipulá-los no lugar certo no seu código, para que você possa articular corretamente as etapas que o operador deve executar.

Em uma empresa, tínhamos operadores de primeira linha que conheciam muito bem o software e mantinham seu próprio "caderno de atividades" que aumentava nosso relatório de erros e soluções sugeridas. Para reconhecer isso, o software começou a incluir, em exceções, links wiki para o livro de execução, para que uma explicação básica estivesse disponível, bem como links para discussões e observações mais avançadas pelos operadores ao longo do tempo.

Se você teve a diciplina para tentar essa técnica, fica muito mais óbvio o que você deve nomear suas exceções no código ao criar a sua própria. NonRecoverableConfigurationReadFailedException se torna um pequeno atalho para o que você está prestes a descrever mais detalhadamente para o operador. Eu gosto de ser detalhado e acho que será mais fácil para o próximo desenvolvedor que tocar no meu código interpretar.

Sir Wobin
fonte
1
+1 Este é um bom sistema. O que é mais importante: ter certeza de que as informações são transmitidas ou usar palavras curtas?
Michael K
3
+1 para eu gosto da solução de inclusão, contexto, problema, solução
WebDev
1
Essa técnica é muito útil. Definitivamente vou utilizá-lo.
Kid Diamond
Contexto são dados desnecessários. Já está presente no rastreamento de pilha. Ter solução é preferível, mas nem sempre possível / útil. A maioria dos problemas é meio que interrompe o aplicativo normalmente ou ignora as operações pendentes e volta ao loop de execução principal do aplicativo, esperando que da próxima vez você tenha sucesso ... O nome da classe de exceção deve ser tal que a solução se torne óbvia, FileNotFoundou ConnectExceptionvocê sabe o que fazer))
gavenkoa
2
@ThomasFlinkow No seu exemplo, os rastreamentos terão init (), execute () e cleanup () no rastreamento de pilha. Com bom esquema de nomenclatura na biblioteca e API limpa / compreensível, você não precisa de explicações sobre cadeias. E falhe rápido, não carregue estado quebrado por todo o sistema. Rastreios e registros de log com ID exclusivo podem explicar o fluxo / estado do aplicativo.
gavenkoa
23

Em esta questão mais recente I fez o ponto que exceções não devem conter uma mensagem a todos. Na minha opinião, o fato de que eles fazem é um enorme equívoco. O que estou propondo é que

A "mensagem" da exceção é o nome da classe (totalmente qualificado) da exceção.

Uma exceção deve conter em suas próprias variáveis-membro o máximo de detalhes possível sobre o que exatamente aconteceu; por exemplo, um IndexOutOfRangeExceptiondeve conter o valor do índice considerado inválido, além dos valores superiores e inferiores válidos no momento em que a exceção foi lançada. Dessa forma, usando a reflexão, é possível criar automaticamente uma mensagem com a seguinte aparência: IndexOutOfRangeException: index = -1; min=0; max=5e essa, juntamente com o rastreamento da pilha, deve ser todas as informações objetivas necessárias para solucionar o problema. A formatação em uma mensagem bonita como "índice -1 não estava entre 0 e 5" não adiciona nenhum valor.

No seu exemplo específico, a NodePropertyNotFoundExceptionclasse conteria o nome da propriedade que não foi encontrada e uma referência ao nó que não continha a propriedade. Isto é importante: deve não conter o nome do nó; deve conter uma referência ao nó real. No seu caso particular, isso pode não ser necessário, mas é uma questão de princípio e uma maneira preferida de pensar: a principal preocupação ao construir uma exceção é que ela deve ser utilizável por código que possa capturá-la. A usabilidade pelos seres humanos é uma preocupação importante, mas apenas secundária.

Isso cuida da situação muito frustrante que você pode ter testemunhado em algum momento de sua carreira, em que você pode ter capturado uma exceção que contém informações vitais sobre o que aconteceu no texto da mensagem, mas não nas variáveis ​​de membro. teve que fazer uma análise de seqüência de caracteres do texto para descobrir o que aconteceu, esperando que o texto da mensagem permaneça o mesmo nas versões futuras da camada subjacente e rezando para que o texto da mensagem não fique em outro idioma quando o programa for executado em outros países.

Obviamente, como o nome da classe da exceção é a mensagem da exceção (e as variáveis ​​de membro da exceção são os detalhes específicos), isso significa que você precisa de muitas e diferentes exceções para transmitir todas as mensagens diferentes, e está bem.

Agora, às vezes, enquanto escrevemos código, encontramos uma situação errônea para a qual queremos codificar rapidamente uma throwinstrução e continuar escrevendo o código em vez de interromper o que estamos fazendo para criar uma nova classe de exceção para que possamos jogá-lo ali mesmo. Nesses casos, tenho uma GenericExceptionclasse que de fato aceita uma mensagem de string como parâmetro de tempo de construção, mas o construtor dessa classe de exceção é adornado com um grande FIXME XXX TODOcomentário roxo brilhante, afirmando que toda instanciação dessa classe deve ser substituído por uma instanciação de alguma classe de exceção mais especializada antes do lançamento do sistema de software, preferencialmente antes do código ser confirmado.

Mike Nakis
fonte
8
Se você estiver em um idioma que não possui um GC, como o C ++, tenha muito cuidado ao colocar referências a dados arbitrários em exceções que você enviar para a pilha. As chances são de que tudo o que você está referenciando tenha sido destruído quando a exceção for detectada.
Sebastian Redl
4
@SebastianRedl True. E o mesmo pode se aplicar a C # e Java se o nodeobjeto for protegido por uma cláusula using-descartable (em C #) ou try-with-resources (em Java): o objeto armazenado com a exceção seria descartado / fechado, tornando-o ilegal acessá-lo para obter informações úteis no local em que a exceção é tratada. Suponho que, nesses casos, algum tipo de resumo do objeto deva ser armazenado dentro da exceção, em vez do próprio objeto. Não consigo pensar em uma maneira infalível de lidar genericamente com isso em todos os casos.
115515 Mike Nakis
13

Como regra geral, uma exceção deve ajudar os desenvolvedores a identificar a causa , fornecendo informações úteis (valores esperados, valor real, possíveis causas / solução, etc.).

Novos tipos de exceção devem ser criados quando nenhum dos tipos internos fizer sentido . Um tipo específico permite que outros desenvolvedores capturem uma exceção específica e a tratem. Se o desenvolvedor souber como lidar com sua exceção, mas o tipo for Exception, ele não poderá lidar com isso corretamente.

mbillard
fonte
+1 - valores esperados versus valores reais são extremamente úteis. No exemplo dado na pergunta, você não deve simplesmente dizer que um método falhou, mas por que falhou (basicamente, o comando exato que falhou e as circunstâncias que causaram a falha).
Felix Dombek
4

No .NET, nunca throw new Exception("...")(como o autor da pergunta mostrou). A exceção é o tipo de exceção raiz e nunca deve ser lançada diretamente. Em vez disso, lance um dos tipos de exceção .NET derivados ou crie sua própria exceção personalizada que deriva de Exception (ou outro tipo de exceção).

Por que não lançar Exception? Porque lançar Exceção não faz nada para descrever sua exceção e força seu código de chamada a escrever um código como o catch(Exception ex) { ... }que geralmente não é uma coisa boa! :-).

bytedev
fonte
2

O que você deseja procurar para "adicionar" à exceção são os elementos de dados que não são inerentes à exceção ou ao rastreamento de pilha. Se isso faz parte da "mensagem" ou precisa ser anexado quando registrado é uma pergunta interessante.

Como você já observou, a exceção proativamente diz o que, o stacktrace provavelmente diz onde, mas o "porquê" pode estar mais envolvido (deveria ser, seria de se esperar) do que apenas olhar em uma linha ou duas e dizer " doh! Claro ". Isso é ainda mais verdadeiro ao registrar erros no código de produção - muitas vezes fui picado por dados ruins que chegaram a um sistema ativo que não existe em nossos sistemas de teste. Uma coisa tão simples quanto saber qual é o ID do registro no banco de dados que está causando (ou contribuindo) para o erro pode economizar uma quantidade significativa de tempo.

Então ... Listado ou, para .NET, adicionado à coleta de dados de exceções registradas (cf @Plip!):

  • Parâmetros (isso pode ser um pouco interessante - você não pode adicionar à coleta de dados se ela não serializar e, às vezes, um único parâmetro pode ser surpreendentemente complexo)
  • Os dados adicionais retornados pelo ADO.NET ou Linq para SQL ou similares (isso também pode ser um pouco interessante!).
  • Qualquer outra coisa pode não ser aparente.

Algumas coisas, é claro, você não saberá que precisa até não as ter em seu relatório / log inicial de erros. Algumas coisas que você não percebe que pode conseguir até achar que precisa delas.

Murph
fonte
0

Quais são exceções para ?

(1) Dizendo ao usuário que algo deu errado?

Esse deve ser o último recurso, porque seu código deve interceder e mostrar a eles algo "melhor" do que uma exceção.

A mensagem "erro" deve indicar clara e sucintamente o que deu errado e o que, se houver, o usuário pode fazer para se recuperar da condição de erro.

ex. "Por favor, não pressione este botão novamente"

(2) Dizendo a um desenvolvedor quando deu errado?

Esse é o tipo de coisa que você faz logon em um arquivo para análise subsequente.
O rastreamento de pilha informará ao desenvolvedor onde o código foi quebrado; a mensagem deve indicar novamente o que deu errado.

(3) Dizendo a um manipulador de exceção (código) que algo deu errado?

O Type of the Exception decidirá qual manipulador de exceções analisará e as propriedades definidas no objeto Exception permitirão que o manipulador lide com ele.

A mensagem da exceção é totalmente irrelevante .

Phill W.
fonte
-3

Não crie novos tipos se puder ajudá-lo. Eles podem causar confusão, complexidade extra e levar a mais código a ser mantido. Eles podem fazer com que seu código precise ser estendido. Projetar uma hierarquia de exceção requer muito pensamento e teste. Não é um pensamento posterior. Geralmente, é melhor usar a hierarquia de exceção de idioma incorporada.

O conteúdo da mensagem de exceção depende do destinatário da mensagem - então você deve se colocar no lugar dessa pessoa.

Um engenheiro de suporte precisará identificar a fonte do erro o mais rápido possível. Inclua uma sequência descritiva curta, além de quaisquer dados que possam ajudar a solucionar o problema. Sempre inclua o rastreamento de pilha - se você puder - essa será a única fonte verdadeira de informações.

A apresentação de erros para usuários gerais do seu sistema depende do tipo de erro: se o usuário puder corrigir o problema, fornecendo entradas diferentes, por exemplo, será necessária uma mensagem descritiva concisa. Se o usuário não conseguir resolver o problema, é melhor declarar que ocorreu um erro e registrar / enviar um erro ao suporte (usando as diretrizes acima).

Além disso - não vomite um grande "HALT ERROR!" ícone. É um erro - não é o fim do mundo.

Então, em resumo: pense nos atores e casos de uso para o seu sistema. Coloque-se no lugar desses usuários. Seja útil. Seja legal. Pense nisso com antecedência no design do sistema. Da perspectiva de seus usuários - esses casos de exceção e como o sistema os trata, são tão importantes quanto os casos normais em seu sistema.

Conor
fonte
10
Discordo. Há muitas boas razões para implementar suas próprias exceções quando a API do idioma não cobre suas necessidades exatas. Uma razão é que, em um método em que várias coisas podem falhar, você pode escrever diferentes cláusulas de captura para diferentes tipos de exceções, nas quais é possível reagir ao problema exato. Outra é que você pode separar várias camadas de exceções que representam diferentes camadas de abstração, onde a camada exata à qual uma exceção pertence pode ser codificada em seu tipo. Basta usar "Exception" ou "IllegalStateException" e uma cadeia de mensagens não ajuda muito lá.
Felix Dombek
1
Eu também discordo. Os tipos de exceção devem fazer sentido para o código de chamada que o consumirá. Por exemplo, se estou chamando uma estrutura e isso causa uma FileDoesNotExistException internamente, isso pode não fazer sentido para mim como um chamador da estrutura. Em vez disso, pode ser melhor criar uma exceção personalizada e transmitir a exceção lançada como a exceção interna.
Bydevev