Tratamento de erros é algo que não é visto com o suficiente cuidado pela maioria dos desenvolvedores de software. Com frequência, quando estou dando consultorias, encontro software sem tratamento de erro, com um tratamento de erro inútil ou ruim. Infelizmente a maioria dos desenvolvedores assume que o software deve funcionar e acabou. Acham-se os melhores desenvolvedores do planeta, já que não criam bugs, e esquecem que há problemas que acontecem mesmo com um software sem bugs. A memória acaba, a conexão com o banco de dados ou com um serviço cai, uma permissão é negada, o ambiente de instalação não é suportado, o navegador onde roda a aplicação não suporta a versão de Javascript que você está usando, e por aí vai. Outros não sabem como fazer o tratamento de erro, só sabem que devem tratar erro de alguma forma, e fazem tratamentos de erros inúteis, que mais atrapalham do que ajudam.

Vamos começar primeiro entendendo o que é um erro. O .Net já usa um nome para definir erros que não deixa margens à duplicidade: Exception, ou “exceção” no português. Exceção, segundo o dicionário Aulete Digital é:

  1. Não correspondência a uma regra.
  2. O que não confirma uma regra ou generalização.

Ou seja, exceções são coisas que acontecem e que não fazem parte de uma regra. A palavra regra ajuda bem a entender o significado. Não é uma regra que seu código tenha bugs (eu espero), assim como não é uma regra que a conexão do banco de dados caia, e não é uma regra que o IIS seja reiniciado enquanto a aplicação está rodando. Todos esses casos são exceções. Quando essas exceções acontecem nosso código precisa saber lidar com elas.

* Update: eu mudei de opinião, os próximos 3 parágrafos ficam para histórico. Hoje em dia prefiro trabalhar com Design By Contract, e lanço exceções para código de negócio.

Agora que já está claro o que é um erro, temos que entender quando lançar uma exceção. Devemos ter claro o seguinte: se algo que aconteceu no software não for uma exceção, mas uma regra, como uma regra de negócios, por exemplo, isso não deve ser tratado como um erro, com uma Exception sendo lançada. Assim, por exemplo, quando o usuário digitar um CPF inválido, em vez de lançar uma CPFInvalidoException, você devolve algum objeto que informe que o CPF é inválido. Afinal, digitar um CPF inválido é uma regra de negócio que deve ser tratada, não é um bug ou condição excepcional.

Exceções no .Net são objetos perigosos, eles podem derrubar a aplicação se não forem tratados, e além disso têm um custo altíssimo de performance. Sempre que for lançar uma exceção avalie se está fazendo isso para deixar claro que há um bug ou condição inesperada.

Há uma regra muito clara para saber se você deve lançar uma exceção ou não: lance erros quando seu método não conseguir fazer o que tem que fazer. Se seu método deve abrir uma conexão e não conseguir, lance um erro. Se o seu método deve dar um desconto a um cliente, e o cliente não existir e portanto o desconto não puder ser dado, lance um erro. Mas não lance um erro se estiver buscando um cliente e ele não existir, afinal em buscas o objeto buscado pode realmente não ser encontrado, e isso é legal.

* Fim do update

E como tratar as exceções de verdade? Certamente não é assim:

static void Main(string[] args)
{
    try
    {
        var minhaClasse = new MinhaClasse();
        minhaClasse.FazAlgo();
    }
    catch (Exception)
    {
        throw;
    }
}

Esse código tem diversos problemas:

  1. Todo o código do método Main está envolto em um blog de Try Catch Finally (TCF). Será que instanciar a classe “MinhaClasse” pode gerar um erro? Talvez não, e então essa linha tinha que estar fora do TCF. Já vi empresas onde todos os métodos estão envoltos em TCF, como se tudo pudesse causar erro. Isso causa uma tremenda confusão no código, além de uma queda na performance. Problema: preguiça de checar onde podem acontecer exceções.
  2. No catch a exceção sendo tratada é a System.Exception. Isso significa que qualquer exceção que aconteça vai ser tratada. Mas será que a classe “MinhaClasse”, que está dentro do try, não gera uma exceção mais específica? Se sim, então esta exceção deveria ser tratada. Problema: preguiça de checar quais exceções são lançadas pelo método FazAlgo, ou talvez o método seja tão complicado, tenha tantas dependências, e não tenha tratamento de erros, que isso não seja claro.
  3. Se realmente for o caso de tratar qualquer exceção, então um simples “catch {}” resolveria, não precisava de um “catch (Exception) {}”.
  4. A exceção não é efetivamente tratada, já que o código simplesmente lança a exceção de volta no throw.

Mas dá para ficar pior. Esse código é pior:

static void Main(string[] args)
{
    try
    {
        var minhaClasse = new MinhaClasse();
        minhaClasse.FazAlgo();
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

Esse código é pior porque ele esconde a exceção. Vejam minha janela de edição Visual Studio:

O erro acontece na linha 15, ao chamar FazAlgo. Se eu rodar o primeiro código, da primeira listagem, tenho o seguinte Stack Trace:

at ConsoleApplication1.MinhaClasse.FazAlgo() in F:\ConsoleApplication1\Program.cs:line 50

at ConsoleApplication1.Program.Main(String[] args) in F:\ConsoleApplication1\Program.cs:line 21

at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)

at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()

at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)

at System.Threading.ThreadHelper.ThreadStart()

Rodando da segunda maneira, o Stack Trace é esse:

at ConsoleApplication1.Program.Main(String[] args) in F:\ConsoleApplication1\Program.cs:line 21

at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)

at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()

at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)

at System.Threading.ThreadHelper.ThreadStart()

Deixei em destaque a diferença. Note que na segunda maneira de trabalhar, fazendo “throw ex”, o erro, que aconteceu na minha linha 50, no método “FazAlgo” da classe “MinhaClasse”, é escondido. Temos somente o erro na linha 21, que é a linha de saída do método Main. Ou seja, em vez de ajudar, atrapalhamos, porque sem tratamento nenhum de erro o resultado seria parecido com o primeiro caso, teríamos claro onde o erro realmente ocorreu: no método “FazAlgo”.

Vamos arrumar então? Arrumando todos os problemas o código fica assim:

static void Main(string[] args)
{
    var minhaClasse = new MinhaClasse();
    try
    {
        minhaClasse.FazAlgo();
    }
    catch (MinhaException ex)
    {
        Console.WriteLine("A MinhaException aconteceu./nDetalhes:{0}/nTentar de novo?", ex.Message);
        if (Console.ReadLine() == "s")
            Main(args);
    }
}

Agora realmente temos a exceção sendo tratada. Se acontecer uma System.Exception a aplicação cai, mas talvez seja exatamente o que eu espero que aconteça. Uma System.Exception significa que aconteceu uma exceção, algo não esperado, e a aplicação pode conter um estado inválido, e não deve mais continuar. Talvez cair seja a opção correta. De qualquer forma, isso deve ser analisado, e não simplesmente tratar System.Exception sempre.

Há mais uma coisa faltando: log dos erros. Uma aplicação profissional que se presa loga suas exceções. Aplicação sem log de erros não é uma aplicação profissional, é amadora. Não fazer um log de erros é falta de profissionalismo. Quando a aplicação falhar em produção, como você vai saber se não tiver um log? Vai pedir um dump e analisar no WinDBG, aquela ferramenta super fácil de usar? Facilite sua vida, logue seus erros.

Mais uma dica: é uma boa prática tratar as exceções no mínimo nas fronteiras da aplicação. Isso significa tratar na interface gráfica, tratar nas camadas de serviço, e em qualquer outra onde exista uma fronteira entre sua aplicação e outra. Se você não fizer isso, vai ter desagradáveis surpresas. Aplicação caindo toda hora, yellow screen of death no ASP.Net, e por aí vai. Com Silverlight, se você devolve um erro da camada de serviço WCF para a camada Silverlight vai receber um belo erro “Not Found” de volta no Silverlight, independentemente do erro. Tudo o que você precisa para uma boa depuração, não é?

A politica de tratamento de erros é algo que deve ser definido logo no começo do desenvolvimento de uma aplicação. Definir onde os erros serão tratados, como serão tratados, onde serão logados, que exceções serão definidas, entre outros assuntos devem ser avaliados o quanto antes, preparando uma estrutura mínima de tratamento de erros. Uma boa opção é usar o Enterprise Library, que já possui o Exception Handling Application Block para ajudar na tarefa. Este bloco de aplicação já me poupou centenas (talvez milhares) de linhas de código (baixe a versão 4.1 da EntLib aqui), sem falar na dor de cabeça. Se você não conhece este bloco sugiro avaliá-lo o quanto antes. Mais ou menos… assim que terminar de ler este post.

Resumindo:

  • Se você não vai tratar a exceção, ou seja, fazer alguma coisa com ela, não coloque TCF no método.
  • Use um framework para te ajudar, como o EntLib.
  • Trate erros nas camadas de fronteira.
  • Logue seus erros.
  • Nunca, nunca, nunca faça “throw ex”.

Boa caçada às exceptions!

Giovanni Bassi

Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.