Propagação de exceção: quando devo capturar exceções?

44

MethodA chama um MethodB que por sua vez chama MethodC.

Não há manipulação de exceção no MethodB ou MethodC. Mas há manipulação de exceção no MethodA.

No MethodC, ocorre uma exceção.

Agora, essa exceção está ocorrendo no MethodA, que lida com isso adequadamente.

O que está errado com isto?

Na minha opinião, em algum momento um chamador executará MethodB ou MethodC, e quando ocorrerem exceções nesses métodos, o que será obtido com o tratamento de exceções nesses métodos, que basicamente é apenas um bloco try / catch / finalmente, em vez de apenas deixar eles borbulham até o chamado?

A declaração ou consenso sobre o tratamento de exceções é lançada quando a execução não pode continuar devido a isso - uma exceção. Entendi. Mas por que não capturar a exceção ainda mais na cadeia, em vez de ter blocos try / catch por todo o caminho.

Entendo quando você precisa liberar recursos. Essa é uma questão completamente diferente.

Daniel Frost
fonte
46
Por que você acha que o consenso é ter uma cadeia de passagem pelas capturas?
Caleth
Com um bom IDE e um estilo de codificação adequado, você pode saber que alguma exceção pode ser lançada quando um método é chamado. Manuseá-lo ou permitir que ele seja propagado é a decisão do chamador. Não vejo nenhum problema com isso.
Hieu Le
14
Se um método não pode lidar com a exceção, e está apenas repetindo novamente, eu diria que é um cheiro de código. Se um método não pode lidar com a exceção e não precisa fazer mais nada quando uma exceção é lançada, não há necessidade de um try-catchbloco.
Greg Burghardt
7
"O que está errado com isto ?" : nothing
Ewan
5
A captura de passagem (que não envolve exceções em tipos diferentes ou algo assim) derrota todo o objetivo das exceções. O lançamento de exceção é um mecanismo complexo e foi construído intencionalmente. Se as capturas de passagem fossem o caso de uso pretendido, tudo o que você precisaria seria implementar um Result<T>tipo (um tipo que armazene um resultado de uma computação ou um erro) e retorne-o de suas funções de lançamento. Propagar um erro na pilha implicaria a leitura de todos os valores de retorno, verificando se é um erro e retornando um erro, se houver.
Alexander

Respostas:

139

Como princípio geral, não pegue exceções, a menos que você saiba o que fazer com elas. Se o MethodC lançar uma exceção, mas o MethodB não tiver uma maneira útil de lidar com isso, deverá permitir que a exceção se propague até o MethodA.

As únicas razões pelas quais um método deve ter um mecanismo de captura e religação são:

  • Você deseja converter uma exceção em outra que seja mais significativa para o chamador acima.
  • Você deseja adicionar informações extras à exceção.
  • Você precisa de uma cláusula catch para limpar os recursos que seriam vazados sem uma.

Caso contrário, capturar exceções no nível errado tende a resultar em código que falha silenciosamente, sem fornecer nenhum feedback útil ao código de chamada (e, finalmente, ao usuário do software). A alternativa de capturar uma exceção e, em seguida, relançar imediatamente, é inútil.

Simon B
fonte
28
@GregBurghardt se seu idioma tem algo parecido try ... finally ..., então use-o, não pegue e
repita novamente
19
"capturar uma exceção e, em seguida, refazê-la imediatamente é inútil" Dependendo do idioma e de como você faz isso, pode ser ativamente prejudicial para a base de código. Muitas vezes, as pessoas que tentam isso removem muitas informações sobre a exceção, como o rastreamento de pilha original. Eu lidei com o código em que o chamador recebe uma exceção que é completamente enganosa sobre o que aconteceu e onde.
JimmyJames
7
"não pegue exceções a menos que você saiba o que fazer com elas". Isso parece razoável à primeira vista, mas causa problemas mais tarde. O que você está fazendo aqui é vazar detalhes da implementação para seus chamadores. Imagine que você está usando um ORM específico em sua implementação para carregar dados. Se você não capturar as exceções específicas desse ORM, mas permitir que elas borbulhem, não será possível substituir sua camada de dados sem quebrar a compatibilidade com os usuários existentes. Esse é um dos casos mais óbvios, mas pode ser bastante insidioso e difícil de entender.
Voo
11
@Voo No seu exemplo você não sabe o que fazer com ele. Envolva-o em uma exceção documentada específica para o seu código, por exemplo, LoadDataExceptione inclua os detalhes da exceção original de acordo com os recursos do seu idioma, para que futuros mantenedores possam ver a causa raiz sem precisar anexar um depurador e descobrir como reproduzir o problema.
Colin Young
14
@Voo Parece que você esqueceu o motivo "Você deseja converter uma exceção em outra que seja mais significativa para o chamador acima" para os cenários de captura / repetição.
jpmc26
21

O que está errado com isto ?

Absolutamente nada.

Agora, essa exceção está ocorrendo no MethodA, que lida com isso adequadamente.

"lida com isso adequadamente" é a parte importante. Esse é o cerne da manipulação de exceções estruturadas.

Se o seu código puder fazer algo "útil" com uma exceção, vá em frente. Se não, então deixe bem em paz.

. . . Por que não capturar a exceção mais adiante na cadeia, em vez de ter blocos try / catch por todo o caminho.

É exatamente o que você deveria estar fazendo. Se você está lendo um código que possui manipuladores / repetidores "até o fim", provavelmente está lendo um código muito ruim.

Infelizmente, alguns desenvolvedores apenas veem os blocos catch como código "boiler-plate" que eles inserem (sem trocadilhos) em todos os métodos que escrevem, geralmente porque eles realmente não "recebem" o Exception Handling e pensam que precisam adicionar algo para que as exceções não "escapam" e matam seu programa.

Parte da dificuldade aqui é que, na maioria das vezes, esse problema nem é percebido, porque as exceções não são lançadas o tempo todo, mas, quando são , o programa vai desperdiçar muito tempo e gradualmente, desmarcando a pilha de chamadas para chegar a algum lugar que realmente faça algo útil com a exceção.

Phill W.
fonte
7
O que é ainda pior é quando o aplicativo captura a exceção e o registra (onde esperamos que não fique parado para sempre) e tenta continuar como de costume, mesmo quando realmente não pode.
Solomon Ucko
1
@SolomonUcko: Bem, isso depende. Se, por exemplo, você estiver gravando um servidor RPC simples e uma exceção não tratada chegar até o loop do evento principal, sua única opção razoável é registrá-lo, enviar um erro RPC ao ponto remoto e retomar o processamento de eventos. A outra alternativa é matar o programa inteiro, o que deixará seus SREs malucos quando o servidor morrer na produção.
Kevin
@ Kevin Nesse caso, deve haver um único catchno nível mais alto possível que registre o erro e retorne uma resposta de erro. Não catchhavia blocos espalhados por toda parte. Se você não deseja listar todas as exceções verificadas possíveis (em linguagens como Java), basta agrupá-las em RuntimeExceptionvez de registrá-las lá, tentar continuar e encontrar mais erros ou até vulnerabilidades.
Solomon Ucko
8

Você precisa fazer a diferença entre bibliotecas e aplicativos.

As bibliotecas podem lançar exceções não capturadas livremente

Quando você cria uma biblioteca, em algum momento você precisa pensar no que pode dar errado. Os parâmetros podem estar no intervalo errado ou null, recursos externos podem estar indisponíveis etc.

Sua biblioteca geralmente não tem como lidar com eles de maneira sensata . A única solução sensata é lançar uma exceção apropriada e deixar que o desenvolvedor do aplicativo lide com ela.

Os aplicativos sempre devem, em algum momento, capturar exceções

Quando uma exceção é capturada, gosto de categorizá-las como erros ou erros fatais . Um erro regular significa que uma única operação no meu aplicativo falhou. Por exemplo, um documento aberto não pôde ser salvo, porque o destino não era gravável. O único pensamento sensato para o Aplicativo é informar ao usuário que a operação não pôde ser concluída com êxito, fornecer informações legíveis por humanos em relação ao problema e deixar o usuário decidir o que fazer em seguida.

Um erro fatal é um erro do qual a lógica principal do aplicativo não pode se recuperar. Por exemplo, se o driver do dispositivo gráfico travar em um videogame, o Aplicativo não poderá "graciosamente" informar o usuário. Nesse caso, um arquivo de log deve ser gravado e, se possível, o usuário deve ser informado de uma maneira ou de outra.

Mesmo em um caso tão grave, o Aplicativo deve lidar com essa exceção de maneira significativa. Isso pode incluir a gravação de um arquivo de log, o envio de um relatório de falha etc. Não há motivo para o aplicativo não responder à exceção de alguma forma.

MechMK1
fonte
De fato, se você tiver uma biblioteca para algo como operações de gravação em disco ou, por exemplo, outra manipulação de hardware, poderá acabar com todos os tipos de eventos inesperados. E se um disco rígido for arrancado durante a gravação? O que uma unidade de CD diminui durante a leitura? Isso está fora do seu controle e, embora você possa fazer algo (por exemplo, fingir que foi bem-sucedido), geralmente é uma boa prática lançar uma exceção ao usuário da biblioteca e deixá-lo decidir. Talvez um HDDPluggedOutDuringWritingExceptionpossa ser tratado e não seja fatal para o aplicativo. O programa pode decidir o que fazer com isso.
VLAZ
1
@VLAZ O que é fatal versus não fatal é algo que o aplicativo precisa decidir. A biblioteca deve contar o que aconteceu. O aplicativo deve decidir como reagir a ele.
MechMK1
0

O que há de errado com o padrão que você descreve é ​​que o método A não tem como distinguir entre três cenários:

  1. O método B falhou de maneira antecipada.

  2. O método C falhou de maneira não prevista pelo método B, mas enquanto o método B estava executando uma operação que poderia ser abandonada com segurança.

  3. O método C falhou de maneira não prevista pelo método B, mas enquanto o método B estava executando uma operação que colocava as coisas em um estado incoerente supostamente temporário, que B não conseguiu limpar devido à falha de C.

A única maneira de o método A conseguir distinguir esses cenários será se a exceção lançada de B incluir informações suficientes para esse fim ou se a pilha que se desenrola para o método B fizer com que o objeto seja deixado em um estado explicitamente inválido . Infelizmente, a maioria das estruturas de exceção torna os dois padrões desajeitados, forçando os programadores a tomar decisões de design "menos graves".

supercat
fonte
2
Os cenários 2 e 3 são bugs no método B. O método A não deve tentar corrigi-los.
Sjoerd
@ Sjoerd: Como o método B deve antecipar todas as maneiras pelas quais o método C pode falhar?
supercat
Por padrões bem conhecidos, como a execução de todas as operações que podem ser lançadas em variáveis ​​temporárias, as operações que não podem ser lançadas - por exemplo, swap - trocam a antiga pelo novo estado. Outro padrão é definir operações que podem ser repetidas com segurança, para que você possa tentar novamente a operação sem medo de estragar. Existem livros completos sobre como escrever 'código de exceção seguro', então não posso contar tudo aqui.
Sjoerd
Este é um bom ponto para não usar exceções (o que seria uma excelente decisão). Mas acho que isso realmente não responde à pergunta, pois o OP parece ter a intenção de usar exceções em primeiro lugar, e apenas pergunta onde deve ser a captura.
cmaster
O Método B do @Sjoerd se torna muito mais fácil de raciocinar sobre se as exceções são proibidas pelo idioma. Como, nesse caso, você realmente vê todos os caminhos de fluxo de controle por meio de B e não precisa adivinhar quais operadores podem estar sobrecarregados de maneira arriscada (C ++) para evitar o cenário 3. Estamos pagando muito em termos de código clareza e segurança para ter preguiça de retornar erros "apenas" lançando exceções. Porque, no final, o tratamento de erros é uma parte vital do código.
cmaster