
Fonte
A manipulação de erros em qualquer desenvolvimento desempenha um papel crucial. Quase tudo pode dar errado no programa: o usuário insere dados incorretos, ou eles podem ser encontrados via http, ou cometemos um erro ao escrever serialização / desserialização e durante o processamento, o programa trava com um erro. Sim, pode ficar sem espaço em disco.
spoiler¯_ (ツ) _ / ¯, não existe um caminho único e, em cada situação específica, você terá que escolher a opção mais adequada, mas há recomendações sobre como fazê-lo melhor.
Prefácio
Infelizmente (ou apenas uma vida assim?), Esta lista continua. O desenvolvedor precisa constantemente pensar no fato de que em algum lugar pode ocorrer um erro e existem duas situações:
- quando o erro esperado ocorre ao chamar a função que fornecemos e podemos tentar processar;
- quando ocorrer um erro inesperado durante a operação que não previmos.
E se os erros esperados forem pelo menos localizados, o resto poderá acontecer em quase todos os lugares. Se não processarmos nada importante, podemos simplesmente travar com um erro (embora esse comportamento não seja suficiente e você precise pelo menos adicionar uma mensagem ao log de erros). Mas se, no momento, o pagamento está sendo processado e você simplesmente não pode cair, mas pelo menos precisa retornar uma resposta sobre a operação malsucedida?
Antes de procurarmos maneiras de lidar com erros, algumas palavras sobre Exceção (exceções):
Exceção

Fonte
A hierarquia de exceções é bem descrita e você pode encontrar muitas informações sobre ela, portanto não faz sentido pintá-la aqui. O que ainda causa discussões acaloradas é checked
e erros unchecked
verificados. E embora a maioria aceite exceções unchecked
como preferidas (no Kotlin não há exceções checked
), nem todos concordam com isso.
As exceções checked
realmente tinham uma boa intenção de torná-las um mecanismo conveniente de tratamento de erros, mas a realidade fez seus ajustes, embora a idéia de introduzir todas as exceções que podem ser lançadas dessa função na assinatura seja compreensível e lógica.
Vejamos um exemplo. Suponha que tenhamos uma função de method
que possa PanicException
uma PanicException
verificada. Essa função ficaria assim:
public void method() throws PanicException { }
Pela descrição dela, fica claro que ela pode lançar uma exceção e que pode haver apenas uma exceção. Parece bastante confortável? E enquanto temos um pequeno programa, é isso. Mas se o programa for um pouco maior e houver mais funções desse tipo, alguns problemas aparecerão.
As exceções verificadas exigem, por especificação, que todas as possíveis exceções verificadas (ou um ancestral comum para elas) sejam listadas na assinatura da função. Portanto, se tivermos uma cadeia de chamadas a
-> b
-> c
a função mais aninhada gera algum tipo de exceção, ela deve ser descartada para todos na cadeia. E se houver várias exceções, a função superior na assinatura deverá ter uma descrição de todas elas.
Portanto, à medida que o programa se torna mais complexo, essa abordagem leva ao fato de que as exceções na função superior desmoronam gradualmente para ancestrais comuns e acabam se resumindo à Exception
. O que neste formulário se torna semelhante a uma exceção unchecked
e anula todas as vantagens das exceções verificadas.
E dado que o programa, como organismo vivo, está constantemente mudando e evoluindo, é quase impossível prever antecipadamente quais exceções podem surgir nele. E, como resultado, a situação é que, quando adicionamos uma nova função com uma nova exceção, precisamos passar por toda a cadeia de uso e alterar as assinaturas de todas as funções. Concordo que essa não é a tarefa mais agradável (mesmo considerando que os IDEs modernos fazem isso por nós).
Mas a última e provavelmente a maior unha nas exceções verificadas “dirigiu” lambdas do Java 8. Não há exceções verificadas ¯_ (ツ) _ / ¯ na assinatura (uma vez que qualquer função pode ser chamada em lambda, com qualquer assinatura), portanto, qualquer chamada de função com uma exceção verificada do lambda obriga a ser agrupada em um encaminhamento de exceção como desmarcada:
Stream.of(1,2,3).forEach(item -> { try { functionWithCheckedException(); } catch (Exception e) { throw new RuntimeException("rethrow", e); } });
Felizmente, na especificação da JVM, não há exceções verificadas; portanto, no Kotlin, você não pode agrupar nada na mesma lambda, mas simplesmente chamar a função desejada.
embora às vezes ...Embora isso às vezes leve a consequências inesperadas, como, por exemplo, a operação incorreta do @Transactional
no Spring Framework
, que "espera" apenas exceções não verificadas. Mas isso é mais um recurso do framework, e talvez esse comportamento no Spring mude no futuro próximo do github .
As próprias exceções são objetos especiais. Além do fato de poderem ser "lançados" por meio de métodos, eles também coletam o rastreamento de pilha na criação. Esse recurso ajuda na análise de problemas e na busca de erros, mas também pode levar a alguns problemas de desempenho se a lógica do aplicativo ficar fortemente vinculada a exceções geradas. Conforme mostrado no artigo , desabilitar o conjunto de rastreamento de pilha pode aumentar significativamente seu desempenho nesse caso, mas você deve recorrer a ele apenas em casos excepcionais quando for realmente necessário!
Tratamento de erros
A principal coisa a fazer com erros "inesperados" é encontrar um lugar onde você possa interceptá-los. Nos idiomas da JVM, esse pode ser um ponto de criação de fluxo ou um ponto de filtro / entrada para o método http, onde você pode colocar uma tentativa de captura com o tratamento de erros unchecked
verificados. Se você usar qualquer estrutura, provavelmente já terá a capacidade de criar manipuladores de erro comuns, como, por exemplo, no Spring Framework, você pode usar métodos com a anotação @ExceptionHandler
.
Você pode "criar" exceções para esses pontos centrais de processamento que não queremos manipular em locais específicos, lançando as mesmas exceções não verificadas (quando, por exemplo, não sabemos o que fazer em um local específico e como lidar com o erro). Mas esse método nem sempre é adequado, porque às vezes pode ser necessário tratar o erro no local e você precisa verificar se todos os locais de chamadas de função são processados corretamente. Considere maneiras de fazer isso.
Ainda use exceções e o mesmo try-catch:
int a = 10; int b = 20; int sum; try { sum = calculateSum(a,b); } catch (Exception e) { sum = -1; }
A principal desvantagem é que podemos “esquecer” de envolvê-lo em um try-catch no local da chamada e pular a tentativa de processá-lo no local, por causa do qual a exceção será lançada no ponto comum do processamento de erros. Aqui, podemos acessar checked
exceções checked
(para Java), mas obteremos todas as desvantagens mencionadas acima. Essa abordagem é conveniente, se o tratamento de erros no local nem sempre for necessário, mas em casos raros, é necessário.
Use a classe selada como resultado de uma chamada (Kotlin).
No Kotlin, você pode limitar o número de herdeiros de classe, torná-los computáveis no estágio de compilação - isso permite que o compilador verifique se todas as opções possíveis são analisadas no código. Em Java, você pode criar uma interface comum e vários descendentes, no entanto, perdendo as verificações no nível da compilação.
sealed class Result data class SuccessResult(val value: Int): Result() data class ExceptionResult(val exception: Exception): Result() val a = 10 val b = 20 val sum = when (val result = calculateSum(a,b)) { is SuccessResult -> result.value is ExceptionResult -> { result.exception.printStackTrace() -1 } }
Aqui temos algo como uma abordagem de erro golang
quando você precisa verificar explicitamente os valores resultantes (ou ignorar explicitamente). A abordagem é bastante prática e especialmente conveniente quando você precisa lançar muitos parâmetros em cada situação. A classe Result
pode ser expandida com vários métodos que facilitam a obtenção do resultado com uma exceção lançada acima, se houver (ou seja, não precisamos lidar com o erro no local da chamada). A principal desvantagem será apenas a criação de objetos supérfluos intermediários (e uma entrada um pouco mais detalhada), mas também pode ser removida usando classes inline
(se um argumento for suficiente para nós). e, como um exemplo específico, há uma classe Result
do Kotlin. É verdade que é apenas para uso interno, pois no futuro, sua implementação poderá mudar um pouco, mas se você quiser usá-lo, poderá adicionar o sinalizador de compilação -Xallow-result-return-type
.
Como um dos tipos possíveis da reivindicação 2, o uso do tipo da programação funcional de Either
, que pode ser um resultado ou um erro. O próprio tipo pode ser uma classe sealed
ou uma classe inline
. Abaixo está um exemplo de uso da implementação da biblioteca de arrow
:
val a = 10 val b = 20 val value = when(val result = calculateSum(a,b)) { is Either.Left -> { result.a.printStackTrace() -1 } is Either.Right -> result.b }
Either
deles Either
mais adequado para quem gosta de uma abordagem funcional e gosta de criar cadeias de chamadas.
Use Option
ou tipo nullable
do Kotlin:
fun testFun() { val a = 10 val b = 20 val sum = calculateSum(a,b) ?: throw RuntimeException("some exception") } fun calculateSum(a: Int, b: Int): Int?
Essa abordagem é adequada se a causa do erro não for muito importante e quando for apenas uma. Uma resposta vazia é considerada um erro e é lançada mais alto. O registro mais curto, sem criar objetos adicionais, mas essa abordagem nem sempre pode ser aplicada.
Semelhante ao item 4, usa apenas um valor de código fixo como marcador de erro:
fun testFun() { val a = 10 val b = 20 val sum = calculateSum(a,b) if (sum == -1) { throw RuntimeException(“error”) } } fun calculateSum(a: Int, b: Int): Int
Esta é provavelmente a abordagem mais antiga de tratamento de erros que voltou de C
(ou mesmo de Algol). Não há sobrecarga, apenas um código que não é totalmente claro (junto com restrições na escolha do resultado), mas, ao contrário da etapa 4, é possível criar vários códigos de erro se mais de uma exceção possível for necessária.
Conclusões
Todas as abordagens podem ser combinadas, dependendo da situação, e nenhuma delas é adequada em todos os casos.
Assim, por exemplo, você pode obter uma abordagem golang
para erros usando classes sealed
e, quando isso não for muito conveniente, passar para erros unchecked
verificados.
Ou, na maioria dos lugares, nullable
tipo nullable
como um marcador que não foi possível calcular o valor ou obtê-lo de algum lugar (por exemplo, como um indicador de que o valor não foi encontrado no banco de dados).
E se você tiver um código totalmente funcional, juntamente com a arrow
ou alguma outra biblioteca similar, é mais provável que você use Either
.
Quanto aos servidores http, é mais fácil elevar todos os erros a pontos centrais e somente em alguns lugares combina a abordagem nullable
com classes sealed
.
Ficarei feliz em ver nos comentários que você está usando isso, ou talvez haja outros métodos convenientes de tratamento de erros?
E obrigado a todos que leram até o fim!