Por que eu devo ler este artigo:Este artigo será útil principalmente para desmistificar o tratamento de exceções somente como base nos erros ocorridos pelo framework. Um programa bem formulado no tratamento de possíveis exceções pode se tornar, de uma maneira mais complexa, um programa de mais qualidade por não interromper o seu fluxo quando uma dessas exceções ocorrerem.

Guia do artigo:

Entende-se por exceções não somente os erros providos da ferramenta, mas também os desvios do fluxo principal da sua aplicação. Neste artigo detalharemos uma forma padrão para esse tipo de tratamento, usando exemplos de códigos para evitar justamente que, ao ocorrer um desvio, o usuário seja pego com uma mensagem sem tratamento adequado e fique sem saber o motivo da paralisação do seu sistema.

Hoje em dia, com o desenvolvimento de softwares cada vez mais complexos e integrados com outros sistemas e plataformas, temos um alto índice de informações requisitadas ao usuário.

Quando estas informações são transferidas através destas integrações, surge uma preocupação a mais quando se trata da programação dos recursos, pois os erros que podem ocorrer (ou podemos dizer exceções) são decorrentes, muitas vezes, da falta de algum recurso ou de algum argumento passado de forma inválida.

Não se pode sair desenvolvendo linhas e linhas de códigos sem levar em conta os desvios que possam ocorrer ao longo da execução do sistema. Desenvolver sistemas com certas características sem nos atentar ao tratamento de exceções pode ser motivo de dor de cabeça futuramente para a equipe de desenvolvimento.

Como podemos garantir a integridade do uso de uma aplicação? Afinal, ninguém gosta de ver o seu sistema “travado” durante sua execução.

Quando ocorre uma exceção, o que acontece durante a execução do programa é que o fluxo do mesmo é redirecionado para uma rotina de tratamento dessa exceção. Caso esta não seja tratada, provavelmente surgirão aquelas mensagens na tela do usuário, praticamente incompreensíveis pela maioria deles.

Por isso, é muito importante que os desvios do curso principal do sistema sejam tratados de forma efetiva, afim de que os usuários possam entender realmente o erro na utilização do programa, com uma mensagem mais agradável e efetiva, informando qual foi à regra não cumprida que acarretou a exceção lançada.

Para ficar bem claro, um erro é quando o programa faz uma solicitação e a mesma não é retornada ou o sistema não consegue encontrar o componente ou página solicitada. Já uma exceção, por exemplo, pode se dar quando o usuário digita um valor inválido para um determinado campo.

O tratamento dessas ocorrências é importante para conseguirmos nos comunicar com o usuário, afim de que o mesmo entenda o motivo do lançamento de uma exceção no sistema em determinada ocasião.

Porém, de nada adiantaria executar o desvio de uma possível exceção se a mesmo não fosse tratada de forma conveniente, ou seja, sempre que for feito um desvio, mensagens devem aparecer bem escritas, de forma que a informação do motivo da ocorrência seja de fácil interpretação por parte do usuário do sistema.

Podemos verificar casos em que temos o retorno de uma exceção específica, porém, fora do contexto. Para isso, pode ser feito o tratamento de uma ou mais exceções no mesmo bloco de comando, sempre levando em consideração a ordenação deste tratamento, da exceção mais específica para aquela menos específica.

Caso contrário, se colocarmos uma instrução de tratamento genérica antes de tratarmos as mais específicas, estas jamais serão executadas.

Erros e Exceções

Existem três grandes grupos de erros que podem ocorrer num sistema:

  1. Erros de sintaxe:

    Fazem com que o programa nem execute, ou seja, o erro é retornado na hora da compilação do programa. Estes erros não são passíveis de tratamento via rotina, porém, são descriminado na hora de executar.

  2. Erros em tempo de execução (runtime):

    Estes erros não ocorrem na hora da compilação do sistema, mas sim na hora em que se está usando. Porém, se temos a ideia de que estes erros podem ocorrer, também conseguiremos tratá-los facilmente. Caso não sejam tratados, eles geram uma exceção e param repentinamente a execução do sistema. Este tipo será o nosso objeto de estudo.

  3. Erros lógicos:

    São erros que também podem ser tratados. Deve-se levar em consideração que a lógica de programação pode ter falhas, pois se trata do raciocínio de cada programador. Por isso, este tipo de erro torna-se um dos mais complicados de serem tratados ao logo do desenvolvimento da aplicação, pois o código funcionará sem retorno de erro algum e o compilador também não acusará erro, pois o mesmo não estará na sintaxe do código.

Um exemplo disso é um cálculo feito de forma errada, onde deveria retornar um valor, mas é retornado outro.

Veremos no decorrer deste artigo como capturar estes erros e tratá-los de diferentes formas, utilizado como base a ferramenta Visual Studio.

Tratando exceções com Try/Catch

O grande objetivo do uso do try/catch é que podemos encapsular determinados trechos de códigos que são propensos à ocorrência de exceção.O sistema irá “tentar” a execução dos comandos na ocorrência de uma determinada exceção presentes nesse bloco. O programa será desviado do seu fluxo padrão, direcionando o seguimento para o bloco iniciado com a cláusula catch, que significa captura, ou seja, o sistema captura o erro e dentro desta cláusula que se faz o tratamento do erro/exceção que ocorreu.

A Common Language Runtime (CLR) fica responsável pelo encaminhamento das exceções para a cláusula catch. O seu uso não é obrigatório, porém, a não utilização da mesma e o devido lançamento do tratamento da exceção capturada pode fazer com que o sistema pare a execução de uma forma nociva para o usuário. Na Listagem 1 vemos o código padrão da estrutura e na Figura 1 a execução do mesmo.

Listagem 1. Código Padrão.

      try
      {
           Console.WriteLine("Informe um valor 1!");
           int num1 = Convert.ToInt32(Console.ReadLine());
           Console.WriteLine("Informe um valor 2!");
           int num2 = Convert.ToInt32(Console.ReadLine());
       
           int result = num1 / num2;
       
           Console.WriteLine("{0} / {1} = {2}", num1, num2, result);
      }
      catch
      {
           Console.WriteLine("Não é possivel a divisão por 0");
      }
Retornando mensagem de exceção
Figura 1. Retornando mensagem de exceção.

A listagem mostra como fica a sintaxe das cláusulas try/catch, onde o código que se deseja testar a ocorrência de exceções deve ficar envolvidos pelo try {}. O tratamento ou lançamento das exceções deste bloco de comandos, depois de capturados, devem ser desenvolvidos dentro da cláusula catch.

Instruções no bloco Finally

Quando se fala em exceções, por muitas vezes temos que, além de tratar as que ocorrem, forçar o código a executar determinadas rotinas como, por exemplo, a limpeza de um sessão ou fechar uma conexão com o banco de dados. Quando isso se faz necessário, podemos utilizar a cláusula Finally, que executa independentemente da ação ocorrida, ou seja, se um erro for ou não capturado.

A cláusula catch não se faz necessária na criação do tratamento das exceções, porém, ou a cláusula catch ou a cláusula Finally devem ser tratadas depois da cláusula try. Então, a cláusula Finally não deve ser confundida com o tratamento da exceção ocorrida, mas sim como um complemento a captura da exceção, pois ela vai ser sempre ativada, mesmo não ocorrendo nenhum tipo de exceção.

Listagem 2. Try/Finally.

      try
      {
         //Código com possibilidade de exceções
      }
      Finally
      {
         //Código que deve ser executado mesmo na ocorrencia de exceção
      }

No código da Listagem 2 é mostrada a forma de utilização do try/Finally, usado com a funcionalidade de executar determinado bloco de comandos, mesmo na ocorrência de uma exceção, possibilitando assim, que possam ser limpos os recursos utilizados no bloco try.

Listagem 3. Try/Catch/Finally.

    try
      {
       
       Console.WriteLine("Informe um valor 1!");
       int num1 = Convert.ToInt32(Console.ReadLine());
       Console.WriteLine("Informe um valor 2!");
       int num2 = Convert.ToInt32(Console.ReadLine());
       
       int result = num1 / num2;
       
       Console.WriteLine("{0} / {1} = {2}", num1, num2, result);
       
      }
      catch
      {
       Console.WriteLine("Não é possivel a divisão por 0");
      }
      finally
      {
       Console.WriteLine("Executado mesmo na ocorrência de erro.");
      }
      Console.ReadKey();
Lançamento erro e uso do finally
Figura 2. Lançamento erro e uso do finally.

Vemos na Figura 2 a execução da Listagem 3. Repare que mesmo executando o erro e informando o usuário, o bloco de comando dentro da cláusula finally é executado.

Um bom exemplo para mostrar a utilização do finally é a leitura de um arquivo externo. Quando precisamos abrir um arquivo e ler os dados que estão neste e na ocorrência de um erro, um parâmetro deve ser buscado dentro deste arquivo e então fechá-lo. O desenvolvimento deve garantir que este arquivo seja fechado mesmo lançando um erro na execução. Confira na Listagem 4.

Listagem 4. Usando finally para fechar o arquivo.

    
      private void dadosArquivoTexto(string caminho)
      {
       string arquivo;
       StreamReader file;
       file = new StreamReader(caminho, Encoding.Default);
       
       try
       {
        while((arquivo = file.ReadLine()) != null){
         textBox.Text += arquivo + "\r\n";}
       }
       catch
       {
        //Lançamento da Exceção
       }
       finally
       {
        file.Close();
       }
      }

Lançamento de uma exceção

A cláusula try garante que o código será testado e na ocorrência de exceção desviará o fluxo para a cláusula catch, responsável pela captura dessas exceções.

Essa captura é feita através das classes que são responsáveis pela manipulação das exceções ocorridas. No entanto, o responsável por lançar essa exceção efetivamente é a palavra reservada Throw.

Throw indica a ocorrência de uma exceção durante a execução de um programa, ou seja, ela para a execução e lança as informações referentes à exceção.

É geralmente usada dentro do conjunto de blocos Try/Catch, embora isso não seja uma regra, podendo ser lançada a qualquer momento do código.

Listagem 5. Cláusula Throw.

      try
      {
         //Código com possibilidade de exceções
      }
      catch (Exception exc)
      {
         Throw new Exception();
      }

A instrução throw que está declarada na Listagem 5 é uma palavra reservada que dispara uma ocorrência de exceção, ou seja, as exceções são geradas (lançadas) através da utilização desta cláusula.

Usando a cláusula catch é possível capturar várias exceções em tempo de execução, então é importante utilizar sempre o lançamento de uma exceção vinculada a um bloco try/catch.

As exceções são derivadas do objeto System.Exception de uma forma generalizada. Elas também podem ser capturada através deste objeto, sendo o mais genérico encontrado na plataforma.

Ele vai tratar qualquer tipo de erro que ocorra durante a execução do código envolvido pela cláusula try. Havendo a necessidade de implementar mais tratamentos ou especializar esse tratamento de exceções, pode utilizar mais de uma cláusula catch, isso porque vários objetos descendem do objeto System.Exception, o que nos possibilita direcionar as exceções.

Neste momento devemos ter uma atenção redobrada, pois quando falamos em especializar o erro, temos que nos atentar à hierarquia das classes destinadas para este fim, pois como as mesmas descendem da classe Exception, quando formos tratar a especialização devemos fazer isso de forma a percorrer as exceções mais específicas para as mais genéricas.

Para que a exceção lançada a mais específica possível, temos que tratar a captura utilizando o objeto Exception para que a última das cláusulas catch utilizadas permita chegar até o mais específico dos tratamentos.

A especialização das ocorrências de exceção é um fator que ajudar potencialmente na resolução ou entendimento do que ocorreu no sistema, porém, para que esta especialização capture corretamente a exata ocorrência da exceção, temos que estruturar nosso lançamento de uma maneira onde as específicas sejam testadas primeiramente.

Na Listagem 6 veremos como não se deve fazer esse tratamento, Notaremos que atribuímos à primeira cláusula catch o objeto Exception. Sendo assim, as cláusulas que vem subsequentes ao catch genérico não serão alcançadas de forma alguma pela execução do programa, o que vai acarretar um erro extremamente genérico repassado a tela do usuário.

Listagem 6. Código Incorreto.

        try
       {
          //Erro de Argumento núlo
       }
       catch (ArgumentException ex)
       {
          //será alcançada aqui
       }
       catch (ArgumentNullException ex)
       {
          //não será alcançada
       }

Este tipo de tratamento será permitido pelo compilador, embora alguns hoje em dia sugerem alterações a respeito do tratamento, mostrando que os lançamentos específicos não vão ser alcançados na execução e ocorrência de um erro, pois se tratando de sintaxe, não seria um erro, mas sim um erro da própria hierarquia entre as classes de exceções.

Nota: Está exceção é lançada quando existe algum problema no argumento passado para um método, partindo do princípio que nosso método está sendo chamado com um argumento nulo. Mesmo assim, o retorno que teríamos seria somente que há algum problema no argumento repassado.

Na Figura 3 podemos ver que na ferramenta Visual Studio 2010, aborda esse erro com uma mensagem informando a ocorrência deste tratamento de forma errada. Especificando que já existe um lançamento de exceção mais genérico do que o próximo lançamento e que o código nunca vai ser alcançado pela execução, pois ele vai entrar na cláusula mais genérica, é importante sim utilizar a classe genérica Exception, por exemplo, como o último lançamento a ser tratado, assim garantindo que, se acontecer um erro que não foi tratado de forma mais específica, assim mesmo será lançada uma exceção mesmo que de forma macro.

Erro mostrado no Visual Studio 2010
Figura 3. Erro mostrado no Visual Studio 2010.

Então devemos fazer o tratamento utilizando os objetos específicos dos erros que entendemos que podem ocorrer no trecho de código que será envolto a cláusula catch para tratarmos e mostrarmos o erro que realmente ocorreu na tela, e depois sim utilizar a cláusula mais genérica para capturar qualquer outro erro que possa não ter sido tratado.

A quantidade de catch a ser utilizada vai do tratamento que está sendo feito, podendo haver vários lançamentos de erros distintos, conforme a Listagem 7.

Listagem 7. Código Correto.

        try
       {
          //Erro de argumento nulo.
       }
       catch (ArgumentNullException ex)
       {
          //será alcançada aqui
       }
       catch (ArgumentException ex)
       {
          //captura qualquer outra ArgumentException ou filha
       }

Entendendo a estrutura do objeto de exceção

Todas as exceções que podem ser capturadas em tempo de execução tem sua descendência da classe System.Exception. Esta classe serve como mãe de todas as classes de exceções.

Podemos citar alguma dessas classes comuns no desenvolvimento: SystemException, ArgumentException, ArgumentNullException, e assim por diante. Repare que temos duas classes tratando de Argument, isso porque a classe ArgumentNullException descende da classe ArgumentException.

Então, quando falamos em exceções da CRL entendemos que elas são geradas pela System.Exception e que suas classes derivadas servem para especificar a raiz da exceção ocorrida.

Quando surgir a necessidade de controlar as exceções do usuário, é necessário criar uma classe descendente da classe System.ApplicationException.

No desenvolvimento de sistemas, as exceções são tão importantes quanto saber tratá-las e para isso, existem diversos recursos para entendermos o motivo destas exceções e conseguir interpretá-las. São esses os recursos:

  • ToString() – mostrará de que tipo é está exceção, seguida da mensagem e do StackTrace.
  • Message – mostrará uma breve descrição do erro ocorrido.
  • Soucer – mostrará a aplicação onde a exceção ocorreu.
  • StackTrace – mostrará o rastreamento do caminho da ocorrência da exceção.
  • TargetSite – método executado no momento em que se lançou a exceção.
  • InnerException – a exceção pode estar envolvido dentro de outras exceções superiores.
Nota: Sempre que possível, mesmo através da customização da mensagem de erro, é interessante colocar a mensagem que o sistema dispõe mostrando a exata ocorrência do erro, utilizando a propriedade Message das classes derivadas da classe Exception.

Lançando de forma explícita

Foi visto até agora como capturamos uma exceção, mas podemos também lançá-la de forma mais explícita. Hoje, a maioria das ferramentas fornecem facilidades para esse tipo de lançamento, geralmente com alguma palavra reservada que indica esse lançamento explícito, seguido do objeto da exceção.

No nosso caso na plataforma .NET, podemos lançar uma exceção de forma explícita com a utilização da palavra reservada throw seguida da palavra new mais a classe destinada a exceção que está sendo lançada, conforme vemos na Listagem 8.

Nela estamos lançando uma exceção explicitamente informado que o arquivo não foi encontrado e passando o caminho que a requisição está tentando localizar.

Listagem 8. Código Explícito.

      try
      {
        throw new FileNotFoundException();
      }
      catch (FileNotFoundException e)
      {
         Console.WriteLine("[Data File Missing] {0}", e);
           throw new FileNotFoundException(@"[data.txt not in c:\temp directory]", e);            
      }

Quando houver a necessidade de levar a exceção lançada a um nível superior, temos a possibilidade de relançar esta exceção quando ela for tratada, para evitar que este tipo de exceção seja retratada e até perdida pelo caminho.

O relançamento de uma exceção consiste em lançar a exceção no ponto onde ela ocorrer, no método que ela ocorrer, e repassar ela quando tiver a necessidade de enviá-la para frente.

Um exemplo disso é a utilização de um log de mensagens de erro, que temos que tratar a exceção e salvar dentro de um log para depois expor a exceção na tela, ou até mesmo para outro nível. Veja um exemplo na Listagem 9.

Listagem 9. Código Explícito com log de erro.

       try
      {
        throw new FileNotFoundException();
      }
      catch (FileNotFoundException ex)
      {
             LogdeErro(ex);
           throw ex;
      } 

Lançando uma mensagem na exceção

Muitas vezes temos a necessidade de, além de desviar o fluxo principal ao ocorrer uma exceção, informar o usuário do motivo do fluxo não ter chego até o seu final. Temos que tratar as mensagens vindas ao lançar uma exceção, conforme exemplo da Listagem 10.

Listagem 10. Código Mensagem.

        try
       {
          throw new InvalidCastException();
       }
       catch (InvalidCastException ex)
       {
         MessageBox.Show("Conversão de dados inválida " + ex.Message);
       }

O exemplo lançará uma exceção informando o erro de conversão de dados ao usuário. Caso o erro não fosse tratado, a mensagem que apareceria para o usuário seria Specifield cast is not valid. Para que apareça uma mensagem mais agradável podemos utilizar a sintaxe assim, ao concatenar a expressão ex.Message.

Estamos informando juntamente com a mensagem ao utilizador, o motivo interno para a ocorrência da exceção, podendo assim ajudar ao profissional que prestará manutenção no sistema, a identificar o motivo da tal exceção.

Todas as classes que herdam da classe Exception tem uma propriedade denominada StackTrace. Essa propriedade retorna uma string onde está contida a pilha de métodos da chamada atual, juntamente com o número de linha e o arquivo onde ocorreu a exceção.

Pode ser usado para contemplar a exceção lançada juntamente com a mensagem que se encontra na propriedade Message, que retorna o motivo do lançamento desta exceção, como o StackTrace retorna de uma forma técnica, não seria legal utilizá-lo na mensagem que retorna para o usuário final.

Para isso, seria interessante a criação de um log de erro, podendo ser em um arquivo TXT salvo em uma pasta específica da aplicação ou até mesmo um registro em algum banco de dados, onde seria lançada essa string contendo o caminho onde a exceção foi lançada.

Customizando Exceções

Pode ser interessante a criação do nossos próprios objetos que descendem da classe exception para o tratamento das exceções da nossa aplicação. Podemos criar nosso próprio objeto para tratamento de exceções descendente de ApplicationException, assim podemos deixar previamente tratada algumas exceções.

Devemos tratar somente as exceções que é do nosso conhecimento, que sabemos que naquele trecho de código existe uma propensão maior de ocorrer. A finalidade de customizarmos as exceções é também conseguir, por exemplo, instruir o usuário a fazer determinado processo na ocorrência dessas exceções, lembrando que, uma vez não tratada a exceção, retornará uma mensagem confusa ao usuário.

Criando um objeto nosso para esse tratamento, conseguimos então dar mais interatividade a nossa aplicação.

Na verdade, estamos criando uma classe para manipular os erros que podem ocorrer na sua aplicação, igualmente as classes genéricas existentes hoje nos tratamentos de erros.

Listagem 11. Criação Classe de Exceção.

       class MinhaClasseException : ApplicationException
      {
              public  MinhaClasseException() : base ("Numero digitado deve ser diferente de zero!")
              {}
              public MinhaClasseException(string message) : base(message) 
              {}
              public MinhaClasseException(string message, Exception objetoException)
                  : base(message, objetoException)
              { }
      }

Na Listagem 11 está sendo implementada uma classe chamada MinhaClasseException que está estendendo a classe ApplicationException. Nela criamos um método passando uma mensagem já tratada para o usuário e outras duas classes aplicando polimorfismo para que, se tiver necessidade, o desenvolvedor coloque outra mensagem para o usuário, podendo também passar as duas mensagens, uma do erro específico e uma mensagem genérica.

Ou seja, usando o último método, poderia destacar o campo ao qual o valor 0 foi atribuído e também a mensagem genérica da classe, onde informaria que o motivo da exceção é que um valor digitado precisa ser diferente de zero.

Listagem 12. Utilizando Classe exceção customizada.

      try
      {
         int a, b;
         Console.WriteLine("Digite um numero!");
         a = Convert.ToInt16(Console.ReadLine());
         if(a == 0)
           throw new MinhaClasseException();
         Console.WriteLine();
         b = Convert.ToInt16(Console.ReadLine());
         if (a == 0)
           throw new MinhaClasseException();
      }
      catch (MinhaClasseException e)
      {
         Console.WriteLine(e.Message);
      }

No código da Listagem 12 é feita a captura do erro nos dois campos que podem receber um valor que esteja em desacordo com a regra de negócios, para posteriormente fazer o tratamento do erro na cláusula catch, direcionada pela chamada ou lançamento do erro - throw new MinhaClasseException -, que está informando ao CLR que naquele trecho ocorreu um erro destinado a essa classe, trazendo como resultado a Figura 4.

Nota: Neste caso está sendo informado ao sistema o tipo de erro que está ocorrendo utilizando a instrução throw new MinhaClasseException(), lançando a exceção da nova classe criada e no bloco catch que captura apenas repassando a mensagem para a tela.
Exceção lançada na tela
Figura 4. Exceção lançada na tela.

Boas práticas no tratamento de exceções

No tratamento de exceções podemos citar algumas práticas consideradas como excelentes na hora do tratamento das exceções. Não podemos tratar simplesmente as exceções como algo que queremos fazer só para garantir o funcionamento da aplicação.

Uma recomendação que deve ser vista é não capturar de forma alguma aquilo que você não sabe tratar. Se uma exceção ocorrer ao logo dos testes que estão sendo executados e você não souber o que fazer, simplesmente não invente , procure referências para entender o problema e não lance qualquer tipo de exceção só para tratar.

Nestes casos, o interessante é passar uma mensagem genérica informando o usuário da ocorrência desse erro e pedir para ele reporte o erro acontecido para o administrador do sistema, assim evitando aquelas falhas e deixando frustrado o usuário do sistema, podendo também gerar um log utilizando todos os recursos para descobrir de onde surgiu o erro.

Procure prestar atenção nos erros que podem acontecer por falta de informação para o usuário. Por exemplo, ao dividir um valor por 0, sabemos que esse tipo de divisão não é possível, porém, se esta informação será informada para o usuário, então deve ser tratado para não ocorrer o travamento do sistema.

Uma exceção jamais poderá ser usada para desviar o fluxo padrão como parte da execução do sistema, isto porque existem outros meios para isso, e descaracterizaria o motivo pelo qual foi criado este tratamento. Uma exceção só deve ser usada para tratar possíveis erros no sistema.

Evite também o uso de exceções para fazer algum tipo de verificação de código. Sempre que você precisar fazer uma verificação, utilize os componentes responsáveis para isso. Para ver se a conexão foi fechada, não há necessidade de criar uma exceção para finalizar essa conexão, pode ser feito através de um if e se verdade fechar a conexão dentro deste bloco.

Vale também a dica de que, sempre que for criar sua própria classe de exceção, utilize a palavra Exception no final do nome da classe para identificar que a mesma se trata de uma classe que manipula exceções, e ao lançar estas, faça-o de maneira correta, com as pontuações necessárias para a boa interpretação do usuário final.


Confira também