C#: Tratamento de exceções

1. Mecanismo básico do try/catch/finally

O C# utiliza três palavras-chave — try, catch e finally — para construir seu mecanismo de tratamento de exceções. O código que pode lançar uma exceção é colocado no bloco try; o bloco catch captura e trata a exceção; e o bloco finally é sempre executado, independentemente de ter ocorrido uma exceção, sendo comumente usado para a limpeza de recursos.

CSHARP
try
{
    int result = 10 / 0;
}
catch (DivideByZeroException ex)
{
    Console.WriteLine($"Caught exception: {ex.Message}");
}
finally
{
    Console.WriteLine("finally block always executes");
}
TEXT 📖 Somente leitura
Caught exception: Attempted to divide by zero.
finally block always executes

(1) Blocos de captura múltipla

É possível interceptar diferentes tipos de exceções em ordem. Os tipos de exceção mais específicos devem ser colocados antes dos mais gerais.

CSHARP
try
{
    int[] arr = { 1, 2, 3 };
    Console.WriteLine(arr[10]);
}
catch (IndexOutOfRangeException ex)
{
    Console.WriteLine($"Index out of bounds: {ex.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"General exception: {ex.Message}");
}
TEXT 📖 Somente leitura
Index out of bounds: Index was outside the bounds of the array.

2. Tipos comuns de exceção

Tipo de exceção Cenário de acionamento
FormatException Formato de string inválido, por exemplo, int.Parse("abc")
NullReferenceException Acessando um membro de um objeto null
IndexOutOfRangeException Índice da matriz fora do intervalo
DivideByZeroException Número inteiro dividido por zero
OverflowException Estouro aritmético (em contexto verificado)
FileNotFoundException O arquivo não existe
ArgumentException Argumento de método inválido

▶ Exemplo

CSHARP
try
{
    int num = int.Parse("hello");
}
catch (FormatException)
{
    Console.WriteLine("Format exception: string cannot be converted to integer");
}

string s = null;
try
{
    Console.WriteLine(s.Length);
}
catch (NullReferenceException)
{
    Console.WriteLine("Null reference exception: object is null");
}
▶ Experimente
TEXT 📖 Somente leitura
Format exception: string cannot be converted to integer
Null reference exception: object is null

3. Hierarquia das classes de exceção

A relação de herança entre todas as classes de exceção é a seguinte:

TEXT 📖 Somente leitura
Object
  └─ Exception
       └─ SystemException
            ├─ FormatException
            ├─ NullReferenceException
            ├─ IndexOutOfRangeException
            ├─ DivideByZeroException
            ├─ OverflowException
            ├─ FileNotFoundException
            └─ ArgumentException
                 └─ ArgumentNullException

4. Propriedades da classe de exceção

A classe Exception oferece as seguintes propriedades comumente utilizadas para obter informações detalhadas sobre exceções:

Propriedade Descrição
Message Texto legível que descreve a exceção
StackTrace Informações da pilha de chamadas no momento em que a exceção ocorreu
InnerException A exceção interna que causou a exceção atual
Source Nome do aplicativo ou objeto que lançou a exceção
TargetSite O método que lançou a exceção

▶ Exemplo

CSHARP
try
{
    int zero = 0;
    int result = 100 / zero;
}
catch (Exception ex)
{
    Console.WriteLine($"Message: {ex.Message}");
    Console.WriteLine($"Source: {ex.Source}");
    Console.WriteLine($"TargetSite: {ex.TargetSite}");
}
▶ Experimente
TEXT 📖 Somente leitura
Message: Attempted to divide by zero.
Source: ConsoleApp
TargetSite: Void Main()

5. Lançamento de exceções com throw

Use a palavra-chave throw para lançar ativamente uma exceção. Ao relançar uma exceção, throw; preserva o rastreamento de pilha original, enquanto throw ex; reinicia a pilha. Evite usar esta última.

▶ Exemplo

CSHARP
void CheckAge(int age)
{
    if (age < 0)
    {
        throw new ArgumentException("Age cannot be negative", nameof(age));
    }
    Console.WriteLine($"Age: {age}");
}

try
{
    CheckAge(-5);
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.Message);
}
▶ Experimente
TEXT 📖 Somente leitura
Age cannot be negative (Parameter 'age')

(1) Diferença entre throw e throw ex

CSHARP
void InnerMethod()
{
    throw new InvalidOperationException("Internal error");
}

void OuterMethod()
{
    try
    {
        InnerMethod();
    }
    catch (Exception ex)
    {
        throw;
    }
}

try
{
    OuterMethod();
}
catch (Exception ex)
{
    Console.WriteLine(ex.StackTrace);
}
TEXT 📖 Somente leitura
   at InnerMethod() in Program.cs:line 2
   at OuterMethod() in Program.cs:line 10
💡 Dica: Sempre use throw; para relançar exceções. Isso preserva a pilha de chamadas completa, facilitando a localização da causa raiz. throw ex; trunca a pilha até o método atual, perdendo as informações originais sobre a localização da exceção.


6. Filtros de exceção

O C# 6 introduziu filtros de exceção com a cláusula when, permitindo que condições sejam adicionadas a catch. A exceção só é capturada quando a condição resulta em true.

▶ Exemplo

CSHARP
try
{
    throw new HttpRequestException("Network timeout, please retry later");
}
catch (HttpRequestException ex) when (ex.Message.Contains("timeout"))
{
    Console.WriteLine("Caught timeout exception, preparing to retry");
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"Other network exception: {ex.Message}");
}
▶ Experimente
TEXT 📖 Somente leitura
Caught timeout exception, preparing to retry

7. Classes de exceção personalizadas

Crie classes de exceção personalizadas herdando de Exception. Normalmente, é necessário implementar três construtores: um construtor sem parâmetros, um construtor com um parâmetro de mensagem e um construtor com parâmetros de mensagem e de exceção interna.

▶ Exemplo

CSHARP 📖 Somente leitura
class InsufficientBalanceException : Exception
{
    public decimal Balance { get; }
    public decimal Amount { get; }

    public InsufficientBalanceException()
        : base("Insufficient balance") { }

    public InsufficientBalanceException(string message)
        : base(message) { }

    public InsufficientBalanceException(string message, Exception innerException)
        : base(message, innerException) { }

    public InsufficientBalanceException(decimal balance, decimal amount)
        : base($"Insufficient balance: current balance {balance}, required {amount}")
    {
        Balance = balance;
        Amount = amount;
    }
}

class BankAccount
{
    public decimal Balance { get; private set; }

    public BankAccount(decimal balance)
    {
        Balance = balance;
    }

    public void Withdraw(decimal amount)
    {
        if (amount > Balance)
        {
            throw new InsufficientBalanceException(Balance, amount);
        }
        Balance -= amount;
        Console.WriteLine($"Withdrawal successful, balance: {Balance}");
    }
}

var account = new BankAccount(100);
try
{
    account.Withdraw(200);
}
catch (InsufficientBalanceException ex)
{
    Console.WriteLine(ex.Message);
    Console.WriteLine($"Balance: {ex.Balance}, attempted withdrawal: {ex.Amount}");
}
44 linhas de lógica (limite de 40, somente leitura)
TEXT 📖 Somente leitura
Insufficient balance: current balance 100, required 200
Balance: 100, attempted withdrawal: 200

8. Melhores práticas para tratamento de exceções

(1) Intercepte exceções específicas, não apenas exceções em geral

CSHARP
try
{
    int value = int.Parse(input);
    Console.WriteLine(100 / value);
}
catch (FormatException)
{
    Console.WriteLine("Input is not a valid integer");
}
catch (DivideByZeroException)
{
    Console.WriteLine("Cannot input zero");
}
⚠️ Observação: Não utilize blocos catch vazios para suprimir exceções, pois isso dificulta o diagnóstico de problemas.

(2) Não abafar exceções

CSHARP
try
{
    File.WriteAllText("data.txt", content);
}
catch (Exception)
{
}

O código acima oculta todos os erros e é uma prática inadequada. No mínimo, você deve registrar o erro no log ou re-lançá-lo.

(3) Use o finally para liberar recursos

CSHARP
FileStream fs = null;
try
{
    fs = new FileStream("data.txt", FileMode.Open);
    int b = fs.ReadByte();
}
finally
{
    fs?.Dispose();
}

(4) Use a instrução using em vez de try-finally

Os recursos que implementam a interface IDisposable devem dar preferência à instrução using, que chama automaticamente Dispose no final do escopo, garantindo a liberação do recurso mesmo que ocorra uma exceção.

CSHARP
using (var fs = new FileStream("data.txt", FileMode.Open))
{
    int b = fs.ReadByte();
}
📌 Ponto-chave: A instrução using é equivalente à try-finally com uma chamada à Dispose() e é a abordagem preferida para o gerenciamento de recursos.

(5) Envolver exceções com InnerException

CSHARP
try
{
    SaveToFile();
}
catch (IOException ioEx)
{
    throw new ApplicationException("Failed to save data", ioEx);
}

Ao usar InnerException, as informações originais da exceção são preservadas sem perda de contexto.

❓ Perguntas Frequentes

P: Qual é a diferença entre throw; e throw ex;? R: throw; preserva o rastreamento de pilha original; throw ex; reinicializa a pilha para o método atual. Use throw; ao depurar.

P: Quando o bloco finally não será executado? R: Quando o programa for encerrado à força (por exemplo, Environment.FailFast) ou ocorrer um StackOverflowException, o finally pode não ser executado.

P: É possível ter try-finally sem catch? R: Sim. Essa abordagem não lida com a exceção, mas garante a liberação dos recursos; a exceção continua se propagando para os níveis superiores.

P: As classes de exceção personalizadas devem herdar de Exception? R: Sim, todas as classes de exceção devem herdar de Exception (direta ou indiretamente) para que possam ser capturadas por catch.

P: Quais são as vantagens do filtro de exceção when? R: A cláusula when não captura a exceção quando a condição não é atendida, permitindo que a exceção continue se propagando. Isso é mais eficiente e semanticamente mais claro do que verificar condições dentro de catch.

📖 Resumo

📝 Exercícios

  1. Escreva um método int SafeParse(string s) que utilize try-catch para processar FormatException, retornando 0 e exibindo um aviso caso a análise falhe
  2. Crie uma exceção personalizada InvalidGradeException com uma propriedade Grade que seja lançada quando a nota não estiver no intervalo de 0 a 100
  3. Escreva um código que demonstre a diferença entre throw; e throw ex;, exibindo os valores de StackTrace e observando a diferença
  4. Use o filtro de exceção when para implementar: capture FileNotFoundException somente quando o caminho do arquivo começar com "config"; caso contrário, deixe a exceção se propagar
  5. Escreva um método de leitura de arquivo que utilize a instrução using para garantir que StreamReader seja liberado corretamente, lidando com possíveis FileNotFoundException e IOException
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%