Você deve usar ambos. A questão é decidir quando usar cada um .
Existem alguns cenários em que as exceções são a escolha óbvia :
Em algumas situações você não pode fazer nada com o código de erro , e você só precisa tratá-lo em um nível superior na pilha de chamadas , geralmente apenas registrar o erro, exibir algo para o usuário ou fechar o programa. Nesses casos, os códigos de erro exigiriam que você borbulhasse os códigos de erro manualmente nível por nível, o que é obviamente muito mais fácil de fazer com exceções. A questão é que isso é para situações inesperadas e intratáveis .
Ainda assim, sobre a situação 1 (onde algo inesperado e impossível de manusear acontece, você simplesmente não quer registrar), as exceções podem ser úteis porque você pode adicionar informações contextuais . Por exemplo, se eu obtiver uma SqlException em meus auxiliares de dados de nível inferior, vou querer capturar esse erro no nível inferior (onde conheço o comando SQL que causou o erro) para que possa capturar essas informações e relançar com informações adicionais . Observe a palavra mágica aqui: relançar e não engolir .
A primeira regra de tratamento de exceções: não engula exceções . Além disso, observe que minha captura interna não precisa registrar nada porque a captura externa terá o rastreamento de pilha inteiro e pode registrá-lo.
Em algumas situações, você tem uma sequência de comandos, e se algum deles falhar, você deve limpar / descartar recursos (*), seja esta uma situação irrecuperável (que deve ser lançada) ou uma situação recuperável (neste caso, você pode lidar localmente ou no código do chamador, mas você não precisa de exceções). Obviamente, é muito mais fácil colocar todos esses comandos em uma única tentativa, em vez de testar os códigos de erro após cada método e limpar / descartar no bloco finally. Por favor, note que se você quiser que o erro borbulhe (que é provavelmente o que você quer), você nem precisa detectá-lo - você apenas usa o finally para limpar / descartar - você só deve usar catch / retrow se quiser para adicionar informações contextuais (consulte o marcador 2).
Um exemplo seria uma sequência de instruções SQL dentro de um bloco de transação. Novamente, esta também é uma situação "intratável", mesmo se você decidir pegá-la cedo (trate-a localmente em vez de borbulhar até o topo), ainda é uma situação fatal em que o melhor resultado é abortar tudo ou pelo menos abortar um grande parte do processo.
(*) É como o on error goto
que usávamos no antigo Visual Basic
Em construtores, você só pode lançar exceções.
Dito isso, em todas as outras situações em que você está retornando alguma informação sobre a qual o chamador PODE / DEVE fazer alguma coisa , usar códigos de retorno é provavelmente uma alternativa melhor. Isso inclui todos os "erros" esperados , porque provavelmente eles devem ser tratados pelo chamador imediato, e dificilmente precisarão subir muitos níveis na pilha.
É claro que sempre é possível tratar os erros esperados como exceções e capturar imediatamente um nível acima, e também é possível incluir cada linha de código em um try catch e executar ações para cada erro possível. IMO, este é um design ruim, não apenas porque é muito mais prolixo, mas especialmente porque as possíveis exceções que podem ser lançadas não são óbvias sem a leitura do código-fonte - e exceções podem ser lançadas de qualquer método profundo, criando gotos invisíveis . Eles quebram a estrutura do código criando vários pontos de saída invisíveis que tornam o código difícil de ler e inspecionar. Em outras palavras, você nunca deve usar exceções como controle de fluxo, porque isso seria difícil para outras pessoas entenderem e manterem. Pode ficar ainda mais difícil entender todos os fluxos de código possíveis para teste.
Novamente: para uma limpeza / descarte corretos, você pode usar try-finally sem pegar nada .
A crítica mais popular sobre os códigos de retorno é que "alguém pode ignorar os códigos de erro, mas no mesmo sentido, alguém também pode engolir exceções. O tratamento incorreto de exceções é fácil em ambos os métodos. Mas escrever um bom programa baseado em código de erro ainda é muito mais fácil do que escrever um programa baseado em exceções . E se alguém por qualquer motivo decidir ignorar todos os erros (os antigos on error resume next
), você pode fazer isso facilmente com códigos de retorno e não pode fazer isso sem um monte de boilerplate try-catchs.
A segunda crítica mais popular sobre os códigos de retorno é que "é difícil aparecer" - mas isso é porque as pessoas não entendem que as exceções são para situações não recuperáveis, enquanto os códigos de erro não.
Decidir entre exceções e códigos de erro é uma área cinzenta. É até possível que você precise obter um código de erro de algum método de negócios reutilizável e, em seguida, decida agrupá-lo em uma exceção (possivelmente adicionando informações) e deixá-lo borbulhar. Mas é um erro de design presumir que TODOS os erros devem ser lançados como exceções.
Resumindo:
Gosto de usar exceções quando tenho uma situação inesperada, em que não há muito o que fazer e geralmente queremos abortar um grande bloco de código ou até mesmo toda a operação ou programa. Isso é como o antigo "em erro, goto".
Gosto de usar códigos de retorno quando espero situações em que o código do chamador pode / deve realizar alguma ação. Isso inclui a maioria dos métodos de negócios, APIs, validações e assim por diante.
Essa diferença entre exceções e códigos de erro é um dos princípios de design da linguagem GO, que usa "pânico" para situações inesperadas fatais, enquanto as situações normais esperadas são retornadas como erros.
Já sobre o GO, ele também permite múltiplos valores de retorno , o que é algo que ajuda muito no uso de códigos de retorno, já que você pode retornar simultaneamente um erro e algo mais. Em C # / Java, podemos conseguir isso sem parâmetros, Tuplas ou (meu favorito) Genéricos, que combinados com enums podem fornecer códigos de erro claros para o chamador:
public MethodResult<CreateOrderResultCodeEnum, Order> CreateOrder(CreateOrderOptions options)
{
....
return MethodResult<CreateOrderResultCodeEnum>.CreateError(CreateOrderResultCodeEnum.NO_DELIVERY_AVAILABLE, "There is no delivery service in your area");
...
return MethodResult<CreateOrderResultCodeEnum>.CreateSuccess(CreateOrderResultCodeEnum.SUCCESS, order);
}
var result = CreateOrder(options);
if (result.ResultCode == CreateOrderResultCodeEnum.OUT_OF_STOCK)
// do something
else if (result.ResultCode == CreateOrderResultCodeEnum.SUCCESS)
order = result.Entity; // etc...
Se eu adicionar um novo retorno possível em meu método, posso até verificar todos os chamadores se eles estão cobrindo esse novo valor em uma instrução switch, por exemplo. Você realmente não pode fazer isso com exceções. Quando você usa códigos de retorno, geralmente sabe com antecedência todos os erros possíveis e os testa. Com exceções, você geralmente não sabe o que pode acontecer. Encapsular enums dentro de exceções (em vez de genéricos) é uma alternativa (desde que esteja claro o tipo de exceções que cada método lançará), mas IMO ainda é um projeto ruim.