Como tratar erros

Email

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.

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.

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!

Email
Esse post foi publicado em Sem categoria e marcado por Giovanni Bassi. Marcar link permanente.

Sobre Giovanni Bassi

Arquiteto e desenvolvedor, agilista, pai, filho, namorado, escalador, provocador.
Programa porque gosta, e começou a trabalhar com isso porque acha que trabalhar como administrador é meio chato. Por esse motivo sempre diz que nunca mais vai virar gerente de ninguém. E também porque acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é melhor que hierarquia. Mas isso é outro assunto.
Foi reconhecido  Microsoft MVP depois que alguém notou que ele não dormia a noite pra ficar escrevendo artigos, cuidando e participando do .Net Architects, gravando o podcast Tecnoretórica, escrevendo posts no blog e falando o que bem entende no twitter @giovannibassi. E por falar nisso é no twitter que conta pra todos que gerencia de projetos deve ser feita pelo time e não por um gerentes, que greves em TI são coisas sem sentido e que stored procedure com regras de negócio são malígnas.
Você já deve ter percebido (até porque está lá na primeira frase) que Giovanni é agilista. De tanto gostar disso ele trouxe os programas de certificação e treinamento  PSD e PSM da Scrum.org pro Brasil, e por causa deles, do MVP e de algum trabalho que aparece tem que ficar indo pros EUA de vez enquando, coisa que prefere não fazer. (É bem comum você ouvir ele perguntando porque a Scrum.org e a Microsoft não estão na Itália, por exemplo.)
Junto com alguns Jedis criou a Lambda3, que, apesar de ser pequena e de não ser muito comum no Brasil, insiste em fazer projetos e consultoria direito. Por causa da Lambda3 ele tem trabalhado mais do que quando era consultor independente, mas menos do que a maioria das pessoas. Quer dizer, isso se você considerar que os trabalhos junto à comunidade não são trabalho, caso contrário ele trabalha mais que a maioria das pessoas.
Recentemente ele resolveu que merecia viver melhor e ganhar uns anos de vida e desistiu de ser sedentário, fazendo algum barulho de vez em quando com os amigos no twitter com a hashtag #DotNetEmForma. Por causa do convite recente de amigos do lado Open Source (que ele respeita e admira), começou a escalar, e agora está sempre com as mãos machucadas. Mas ainda dá pra programar. Você encontra ele sempre em algum evento, como o TechEd, e o DNAD, mas também outros menos comuns para o pessoal do .NET, como a RubyConf. Nesses eventos, ou ele está vendo palestras, ou batendo papo com alguém, ou codando alguma aplicação que alguém achou que dava pra fazer durante o evento.
  • Fabio Moggi

    Giovanni, o blog está cada vez melhor ;)

  • http://www.israelaece.com/ Israel Aece

    Boas Giovanni,

    Utilizar essa técnica funciona bem quando você conhece a classe e garante que ela não irá disparar exceções no construtor. (sim, sei que isso é uma prática ruim, quando não é bem escrito).

    Mas as próprias classes do .NET FX fazem isso. Como exemplo, veja a classe FileStream, que dispara uma UnauthorizedAccessException/SecurityException caso o usuário que esteja na aplicação não possua privilégio para manipular o arquivo.

    Neste caso, a criação do objeto também deverá estar protegido:

    FileStream fs = null;
    try
    {
    fs = new FileStream("Arquivo.txt", FileMode.Open);
    }
    catch (UnauthorizedAccessException)
    {
    Console.WriteLine("Voce nao tem acesso");
    }
    catch (Exception ex)
    {
    Console.WriteLine("Problemas: {0}", ex.Message);
    }
    finally
    {
    if (fs != null)
    fs.Dispose();
    }

  • http://unplugged.giggio.net/ Giovanni Bassi

    Oi Israel,
    É isso aí. Se há essa possibilidade, temos que tratá-la. No meu exemplo eu assumi que não havia.

  • Alessandro

    Giggio,
    em ASP.NET MVC,
    criar um interceptor para capturar e logar os erros nos controllers seria uma boa opção (seguindo o conselho de tratar nas fronteiras)?

    Att,
    Alessandro

  • http://unplugged.giggio.net/ Giovanni Bassi

    Alessandro, sem dúvida!

  • http://rafanoronha.net/ Rafael Noronha

    Giovanni,

    Deixo a dica para o pessoal trabalhando com asp.net dar uma conferida no ELMAH, um excelente framework de log de erros.
    http://code.google.com/p/elmah/

    Melhor que um bom framework? Um bom framework que você não precisou escrever. =D

  • http://unplugged.giggio.net/unplugged/post/Como-tratar-erros.aspx Wellington

    Olá Giovanni, Valeu pelas dicas cara!
    Seus posts continuam ajudando muito,
    sempre tive muitas dúvidas para tratar erros…
    vou procurar pela dica do Alessandro (interceptor)
    e do Elmah para usar nas fronteiras da aplicação.

    Valeu mesmo!

    Wellington

  • well

    Valeu Giovanni, muito bom esse post!

    Wellington

  • Alessandro

    Giovani,
    no caso de uma exception em um serviço de domínio, por exemplo, eu preciso fazer com que esta exception passe pela entidade de domínio que chamou o serviço e chegue até a interface do usuário para informá-lo e ou logar o erro.
    Qual a melhor maneira de repassar exceptions para camadas superiores como no caso acima?

    Att,
    Alessandro

  • http://unplugged.giggio.net/ Giovanni Bassi

    Alessandro, Se for só para logar o erro no serviço, então você coloca um TCF, loga, e throw, ou dá um throw em outra exceção, com a original na innerexception.
    Na interface você vai ter que definir que tipo de informação vai exibir ao usuário quando tiver uma exceção. Um TCF pega o erro e exibe o que você achar melhor.

  • http://blog.fujiy.net/ Felipe Fujiy

    Já vi casos piores:

    try{}catch(Exception ex)
    {throw new Exception();}

    Absurdo, não?

    Sei que há casos onde devemos lançar uma nova exception, mas não um Exception(base) e ainda mais sem o InnerException.

    Quanto o catch{} ja ouvi falar que é ruim,pois pega umas exceptions SEH, não lembro o que é, mas é algo do Windows

  • http://prodis.pro.br/ Prodis

    @Felipe, você não viu nada ainda (risos).

    Eu já peguei casos assim:
    try
    {
    //código
    }
    catch (Exception)
    {

    }

    Muito útil para quando você quer arrebentar um sistema.

  • Alessandro de Souza

    Giggio,
    quando uso um TCF e no C eu chamo o throw; a mesma exception é repassada pra cima? É assim que funciona?

    Att,
    Alessandro

  • http://unplugged.giggio.net/ Giovanni Bassi

    Putz, Prodis, já vi assim também. É o "programador coveiro", que quer enterrar o erro…

    Alessandro, é isso mesmo. Ao dar só "throw;", ele lança de volta a exceção inicial.

  • http://twitter.com/brunao05 Bruno Feliciano

    Isso não é nada… Já vi um sistema (numa famosa empresa pública de Processamento de Dados do Estado de São Paulo) que o sistema foi desenvolvido de uma forma que lugares aonde teriam um simples If / Else eram tratados com TCF com catchs genéricos (por exemplo ao invés de verificar se um valor era nulo pra saber oq fazer esperava-se dar uma NullException e no catch estava o codigo que seria o Else…

    Isso é tão absurdo que eu talvez não acreditasse se não tivesse (infelizmente) realmente visto…

  • Guilherme de Carvalho

    Muito interessante o assunto, realmente é algo pouco tratado, creio até que poderia ser um artigo na .net magazine quem sabe.

    Mas no caso de tratamento de erros ao acessar bancos, seria interessante fazer assim(VB):

    Dim conexaoPgSQL as new NpgsqLConnnection(stringConexao)
    Dim comandoPgSQL as new NpgsqlCommand
    Try
    comandoPgSQL.Connection = conexaoPgSQL
    conexaoPgSQL.Open()



    Catch exPgSQL as NpgsqlException
    conexaoPgSQL.Close()
    Catch ex as Exception
    conexaopgSQL.Close()
    End Try

    Ou não seria necessário o segundo Catch (Exception), poderia também colocar dentro dos catch algo para criar log’s de erro?

  • http://unplugged.giggio.net/ Giovanni Bassi

    Guilherme, nem um nem outro.
    Coloca no final do TCF:

    Finally
    If ConexaopgSQL IsNot Nothing Then
    ConexaopgSQL.Close()
    End If
    End Try

    (e eu ainda lembro VB!)

  • Vinicius

    Suponha que eu tenha uma regra no meu sistema que me impeça de remover uma entidade que tenha algum relacionamento com alguma outra entidade do sistema. Devo tratar isso no metodo de exclusao e disparar uma exception caso ocorra ou devo tratar na aplicacao e se for o caso nem chamar a exclusao e mostrar uma mensagem amigavel pro usuario?

    Voce comentou do cpf, nao sei se encaixa na minha duvida,