C# - Capturando e tratando Exceptions (revisão)


As exceções (Exceptions) são mecanismos primários para comunicar condições de erros.

As exceções possuem um grande poder e um grande poder trás também grandes responsabilidades.

Dessa forma não devemos abusar deste recurso mas saber usá-lo com bom senso.

Vou apresentar um resumo bem objetivo de como usar o recurso das exceções acrescentando assim mais esse artigo aos que já escrevi o sobre o assunto.

1- Lançando uma Exception

Quando você precisar indicar que ocorreu um erro em uma determinada situação que não pode ser tratado no nível de código atual você pode lançar a exceção para ser tratada em outro nível usando a declaração throw.

A declaração throw é usada para sinalizar a ocorrência de uma situação anormal (exceção) durante a execução do programa.

A exceção gerada é um objeto cuja classe é derivada de System.Exception.

private void Teste(string valor)
{
if (string.IsNullOrEmpty(value))
{
 
 throw new ArgumentNullException(“valor”, “o valor do parâmetro não pode ser null”);
}

...
}

Você pode lançar explicitamente uma exceção usando a instrução throw. Você também pode lançar uma exceção capturada novamente usando a instrução throw. É boa prática de codificação adicionar informações a uma exceção que é relançada para fornecer mais informações quando da depuração.

Geralmente a instrução throw é usada com um bloco try/catch/finally.

O exemplo a seguir usa um bloco try/catch para capturar uma exceção do tipo FileNotFoundException.

Após o bloco try há o bloco catch que captura a FileNotFoundException e grava uma mensagem para o console se o arquivo de dados não foi encontrado. A declaração seguinte é a instrução throw que lança um FileNotFoundException nova e adiciona informações de texto para a exceção.

public static void Main()
      {
      FileStream fs = null;
      try  
      {
         //Abre o arquivo texto
         fs = new FileStream(@"C:\teste\dados.txt", FileMode.Open);
         StreamReader sr = new StreamReader(fs);
         string line;

         //Um valor é lido do arquivo e exibido no console
         line = sr.ReadLine();
         Console.WriteLine(line);
      }
      catch(FileNotFoundException e)
      {
         Console.WriteLine("[Arquivo de dados não existe] {0}", e);
         throw new FileNotFoundException(@"[Arquivo dados.txt não existe na pasta c:\teste]",e);
      }
      finally
      {
         if (fs != null)
            fs.Close();
      }
   }

De forma prática você deve lançar uma exceção quando uma das seguintes condições ocorrer:

  1. O método não conseguiu completar a sua funcionalidade definida
if (original == null)
    {
       throw new System.ArgumentException("Parâmetro não pode ser null", "original");
    }
  1. Uma chamada inadequada a um objeto é feita, com base no estado do objeto.
    void WriteLog()
    {
        if (!this.logFile.CanWrite)
        {
            throw new System.InvalidOperationException("O arquivo Logfile não pode ser somente leitura");
        }
    }
  1. Quando um argumento para um método faz com que ocorra uma exceção
    try
    {
        return array[index];
    }
    catch (System.IndexOutOfRangeException ex)
    {
        System.ArgumentException argEx = new System.ArgumentException("Indice fora do intervalo", "index", ex);
        throw argEx;
    }

Exceções contém uma propriedade chamada StackTrace. Esta string contém o nome dos métodos na pilha de chamada atual, juntamente com o nome do arquivo e o número da linha onde a exceção foi lançada para cada método.

Um objeto StackTrace é criado automaticamente pelo Common Language Runtime (CLR) do ponto da instrução throw, de modo que as exceções devem ser lançados a partir do ponto onde o rastreamento de pilha deve começar.

Todas as exceções contém uma propriedade chamada Message.

Essa string deve ser definida para explicar a razão para a exceção. Note que a informação que é sensível à segurança não deve ser colocada no texto da mensagem. Além de Message, ArgumentException contém uma propriedade chamada ParamName que deve ser definida para o nome do argumento que causou a exceção a ser lançada. No caso de uma propriedade setter, ParamName deve ser definido como value.

Métodos Public e Protected devem lançar exceções sempre que eles não puderem concluir suas funções pretendidas. A classe execption que é lançada deve ser a exceção mais específica disponível que se adapta às condições de erro. Essas exceções devem ser documentados como parte da funcionalidade da classe, e as classes derivadas ou atualizações para a classe original devem manter o mesmo comportamento para compatibilidade com versões anteriores.

O que você deve evitar quando lançar exceções:

2- Capturando Exceções

Quando você precisar tratar exceções que foram lançadas você deve envolver o código que pode potencialmente lançar uma exceção no interior de um bloco try{ } seguido por um bloco catch { }:

try
{
    FazAlgumaCoisa(null);
}
catch (ArgumentNullException ex)
{
    Console.WriteLine(“Exception: “ + ex.Message);
}

A instrução try-catch consiste em um bloco try seguido por uma ou mais cláusulas catch, que especifica manipuladores para exceções diferentes. Quando uma exceção é lançada, o Common Language Runtime (CLR) procura a instrução catch que trata essa exceção.

Se o método atualmente em execução não contiver um bloco catch, o CLR olha para o método que chamou o método atual, e assim por diante na pilha de chamadas. Se não for encontrado nenhum bloco catch, o CLR exibe uma mensagem de exceção sem tratamento para usuário e interrompe a execução do programa.

O bloco de try contém o código protegido que pode causar a exceção. O bloco é executado até que uma exceção seja gerada ou ele seja concluído com sucesso. Por exemplo, a seguinte tentativa de converter um objeto null gera a exceção NullReferenceException:

object o2 = null;
try
{
    int i2 = (int)o2;   // Erro
}

Embora a cláusula catch possa ser usada sem argumentos para capturar qualquer tipo de exceção, esse uso não é recomendado. Em geral, você só deve capturar essas exceções que você sabe como recuperar. Portanto, você sempre deve especificar um argumento de objeto derivado de System.Exception Por exemplo:

catch (InvalidCastException e) 
{
}

É possível usar mais de uma cláusula catch específica na mesma instrução try-catch. Neste caso, a ordem das cláusulas catch é importante porque as cláusulas catch são examinadas em ordem.

Capture as exceções mais específicas antes das menos específicas.

O compilador irá gerar um erro se você ordenar seus blocos catch de forma que um bloco catch posterior nunca seja alcançado.

try
{
   throw new ArgumentNullException();
}
catch (ArgumentException ex)
{
   //será alcançada aqui
}
catch (ArgumentNullException ex)
{
   //não será alcançada
}

Como ArgumentNullException é um tipo de ArgumentException, e ArgumentException é a primeira na lista de captura, ela será chamada primeiro, de forma que ArgumentNullException nunca será alcançada.

Corrigindo a ordem temos:

try
{
   throw new ArgumentNullException();
}
catch (ArgumentNullException ex)
{
   //será alcançada aqui
}
catch (ArgumentException ex)
{
   //captura qualquer outra ArgumentException ou filha
}

3- Relançando uma Exception

Se você precisar lidar com uma exceção em um nível (registrar um log por exemplo) e em seguida, repassar a exceção para um nível superior de código para ser tratada, você deve relançar a exceção.

Existem duas maneiras de resolver este problema.

A solução mais simples e ingênua , que geralmente esta incorreta, é esta:

try
{
    DoSomething();
}
catch (ArgumentNullException ex)
{
    LogException(ex);
    // a forma incorreta de relançar uma exceção
    throw ex;   //relança a exceção para um nível superior
}

O que há de tão errado com isso ? 

Sempre que uma exceção é lançada, a localização atual da pilha é salva para a exceção. Quando você relançar uma exceção da forma mostrada acima você substitui a localização da pilha que estava originalmente na exceção com a que foi capturada neste bloco catch e provavelmente não é isso o que você queria fazer.

Se você quer relançar uma exceção preservando a chamada original da pilha, lance a exceção sem usar a variável exception.

A seguir temos a forma correta:

try
{
    DoSomething();
}
catch (ArgumentNullException ex)
{
    LogException(ex);
    // a forma correta
    throw;   //relança a exceção para um nível superior e preserva a pilha
}

Fique esperto quando interceptar uma exceção.

Você não vai querer logar uma exceção e então relançá-la para múltiplos níveis fazendo com que o mesmo erro seja logado mais de uma vez .

Muitas vezes, o registro deve ser tratado somente no nível mais elevado. Além disso, cuidado com a armadilha de fazer o tratamento de exceções no nível mais baixo. Se você não pode dar um tratamento inteligente para a exceção, basta deixar um nível mais elevado se preocupar com o seu tratamento.

4- Garantindo a execução com Finally

Você quer garantir que os recursos usados sejam liberados, mesmo quando ocorrerem exceções.

Frequentemente, quando você usar objetos que encapsulam recursos externos (como conexões de banco de dados ou arquivos), vai querer garantir que os recursos sejam liberados quando o trabalho for terminado.

Você quer garantir que os recursos usados sejam liberados, mesmo quando ocorrerem exceções. No entanto, se uma exceção for lançada enquanto você estiver usando os recursos, a exceção normalmente irá ignorar qualquer código para liberar os recursos você tenha escrito.

Use o bloco finally que garante que o código definido no bloco sempre será executado mesmo ocorrendo uma exceção.

O bloco finally é útil para limpar todos os recursos que foram alocados em um bloco try. O controle sempre será passado para o bloco finally e o código definido no bloco sempre será executado.

Normalmente, as declarações de um finally bloco são executadas quando o controle sai de um bloco try, se a transferência de controle ocorre como resultado da execução normal, da execução de um break, continue, goto, ou return de instrução, ou de propagação de uma exceção da try instrução.

StreamWriter stream = null;
try
{
    stream = File.CreateText(“temp.txt”);
    stream.Write(null, -1, 1);
}
catch (ArgumentNullException ex)
{
   Console.WriteLine(“No catch: “);
   Console.WriteLine(ex.Message);
}
finally
{
   Console.WriteLine(“No finally: Fechando o arquivo”);
   if (stream != null)
   {
      stream.Close();
   }
}

Um bloco catch não é necessário quando um bloco finally for usado.

Existe uma situação quando o bloco finally não é executado : Se o seu código forçar uma saída imediata usando Exit() o bloco finally não será executado:

try
{
//faz algo
Environment.Exit(1)   // o programa encerra AGORA
}
finally
{
   //este código nunca será executado
}

5- Obtendo informações importantes de uma exceção

Como obter informações importantes de uma exceção ?

As exceções são objetos ricos em informação. A tabela abaixo lista as propriedades/métodos disponíveis para todos os tipos de exceções:

Propriedade/Método Descrição
ToString() Imprime o tipo de exceção, seguido da mensagem e StackTrace.
Message Uma breve descrição do erro.
Source A aplicação onde a exceção ocorreu.
StackTrace A lista dos métodos na pilha atual. Útil para rastrear o caminho que causou a exceção.
TargetSite O método que lançou a exceção
InnerException A exceção que causou a exceção atual. Muitas vezes, as exceções são envolvidas no interior de outras exceções de nível superior.
HelpLink Um link para um arquivo de ajuda, muitas vezes na forma de uma URL.
Data Pares chave-valor de exceções especificas que fornecem mais informações.

Exemplo de uso:

using System;
using System.Collections;

namespace Excecoes
{
    class Program
    {
        static void Main(string[] args)
        {
             try
            {
                int divisor = 0;
                Console.WriteLine("{0}", 13 / divisor);
              
            }
            catch (DivideByZeroException ex)
            {
                Console.WriteLine("ToString()     : " + ex.ToString());
                Console.WriteLine("Message        : " + ex.Message);
                Console.WriteLine("Source         : " + ex.Source);
                Console.WriteLine("HelpLink       : " + ex.HelpLink);
                Console.WriteLine("TargetSite     : " + ex.TargetSite);
                Console.WriteLine("Inner Exception: " + ex.InnerException);
                Console.WriteLine("Stack Trace    : " + ex.StackTrace);
                Console.WriteLine("Data           : ");
                if (ex.Data != null)
                {
                    foreach (DictionaryEntry de in ex.Data)
                    {
                        Console.WriteLine("\t{0}: {1}", de.Key, de.Value);
                    }
                }
                Console.ReadKey();
            }
        }
    }
}

6- Capturando exceções não tratadas

Como obter um manipulador personalizado para todos e quaisquer exceções lançadas por seu aplicativo que não são tratados pelo mecanismo normal do bloco try...catch mecanismo.

Uma exceção lançada é passada para chamada da pilha até encontrar um bloco que a trate. Se nenhum tratamento for possível o processo irá encerrar a aplicação.

Felizmente, existe uma maneira de capturar as exceções não tratadas e executar o seu próprio código antes da aplicação terminar (ou até mesmo impedi-la de terminar). A maneira de fazer isso depende do tipo de aplicação. A seguir as possibilidades:

a - Capturando exceções não tratadas em uma aplicação Console

Em programas console, você pode ouvir o UnhandledException para a AppDomain atual:


using System;

namespace Excecoes
{
    class Program
    {
        static void Main(string[] args)
        {
            AppDomain.CurrentDomain.UnhandledException +=  new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
            throw new InvalidOperationException("Deu pau...");
        }
        
        static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            Console.WriteLine("Capturando a exceção não tratada");
            Console.WriteLine(e.ExceptionObject.ToString());
        }
    }
}

b - Capturando exceções não tratadas em uma aplicação Windows Forms

No Windows Forms, antes de qualquer outro código ser executado, você deve dizer ao objeto Application que pretende lidar com as exceções não detectadas. Então, você pode ouvir uma ThreadException no segmento principal.

Crie uma aplicação Windows forms que contenha apenas um controle Button e declare no arquivo Program.cs :

using System;
using System.Windows.Forms;

namespace WinFormsExceptions
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            //Este código deve ser executado antes de criar qualquer elemento da UI
            Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

No formulário form1.cs declare o seguinte código:

using System.Text;
using System.Windows.Forms;

namespace WinFormsExceptions
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            //trata qualquer exceção que ocorra nesta thread
            Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
        }

        void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("Trapped unhandled exception");
            sb.AppendLine(e.Exception.ToString());
            MessageBox.Show(sb.ToString());
        }

        private void button1_Click(object sender, System.EventArgs e)
        {
            throw new System.InvalidOperationException("Deu Pau...");
        }
    }
}

c - Capturando exceções não tratadas em uma aplicação WPF

Em uma aplicação WPF escutamos exceções não tratadas no dispatcher:

Crie uma aplicação do tipo WPF Application com um controle Button e defina o seguinte código no arquivo MainWindow.xaml.cs:

using System.Text;
using System.Windows;

namespace Wpf_Execptions
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        
   Application.Current.DispatcherUnhandledException += new System.Windows.Threading.DispatcherUnhandledExceptionEventHandler(Current_DispatcherUnhandledException);
        }
      
        void Current_DispatcherUnhandledException(object sender,System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
        {
                StringBuilder sb = new StringBuilder();
                sb.AppendLine("Caught unhandled exception");
                sb.AppendLine(e.Exception.ToString());
                MessageBox.Show(sb.ToString());
                e.Handled = true;//prevent exit
        }

        private void button1_Click_1(object sender, RoutedEventArgs e)
        {
               throw new InvalidOperationException("Deu pau...");
        }  
    }
}

d - Capturando exceções não tratadas em uma aplicação ASP .NET

Em aplicações ASP.NET, você pode capturar exceções não tratadas quer a nível de página ou a nível de aplicação.

Para pegar erros a nível da página, procure no evento da página de erro.

Para capturar erros a nível de aplicação, você deve ter uma classe de aplicativo global (muitas vezes no Global.asax) e colocar o seu comportamento em Application_Error, como abaixo:

public class Global : System.Web.HttpApplication
{
...
protected void Application_Error(object sender, EventArgs e)
{
 
   //envia para uma aplicação com tratamento de erro
    Server.Transfer(“ErrorHandlerPage.aspx”);
}
...
}

Na página:

public partial class _Default : System.Web.UI.Page
{
   protected void Page_Load(object sender, EventArgs e)
   {
     Button1.Click += new EventHandler(Button1_Click);
     //descomente para tratar o erro a nível de pagina
     //this.Error += new EventHandler(_Default_Error);

   }
   void Button1_Click(object sender, EventArgs e)
   {
       throw new ArgumentException();
   }
}

Com essa pequena compilação sobre exceções procurei dar uma visão geral sobre o tratamento e os aspectos mais importantes sobre esse importante tópico.

João 10:9 Eu sou a porta; se alguém entrar a casa; o filho fica entrará e sairá, e achará pastagens.

João 10:10 O ladrão não vem senão para roubar, matar e destruir; eu vim para que tenham vida e a tenham em abundância.

Referências:


José Carlos Macoratti