1. Anatomia completa: try/catch/finally
try
{
var resultado = 10 / int.Parse("0");
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Divisão por zero: {ex.Message}");
}
catch (FormatException ex)
{
Console.WriteLine($"Formato inválido: {ex.Message}");
}
finally
{
Console.WriteLine("Sempre executa, com ou sem exceção");
}
| Bloco | Quando executa |
|---|---|
try | Sempre, até o ponto onde uma exceção ocorre (se ocorrer) |
catch | Só se uma exceção do tipo declarado (ou subtipo) foi lançada |
finally | Sempre — com exceção capturada, sem capturar, ou até com return no meio do try |
Blocos catch são avaliados de cima para baixo, e o
primeiro que corresponde ao tipo da exceção (ou a um tipo base dela) é usado.
Um catch (Exception ex) genérico colocado antes de
um catch mais específico faz o específico nunca ser alcançado — e
o compilador acusa erro exatamente por isso.
2. A hierarquia de exceções
Toda exceção em C# deriva, direta ou indiretamente, de System.Exception.
Capturar pelo tipo mais específico possível (em vez de sempre Exception)
deixa claro qual falha você realmente sabe tratar:
Fig. 1 — Capturar ArgumentNullException também captura via catch (ArgumentException), mas não o contrário.
3. Exceções customizadas
Para erros específicos do seu domínio, crie uma classe de exceção própria — é exatamente o
padrão que fechou o capítulo 13 do Módulo 2 (SQL), quando um SqlException
com número 50010 virava uma EstoqueInsuficienteException:
public class EstoqueInsuficienteException : Exception
{
public int ProdutoId { get; }
public EstoqueInsuficienteException(int produtoId, string mensagem)
: base(mensagem)
{
ProdutoId = produtoId;
}
}
// lançando
if (produto.Estoque < quantidadeDesejada)
throw new EstoqueInsuficienteException(produto.Id, $"Estoque insuficiente para o produto {produto.Id}");
// capturando especificamente
try
{
FinalizarCompra(pedido);
}
catch (EstoqueInsuficienteException ex)
{
Console.WriteLine($"Produto {ex.ProdutoId} sem estoque suficiente");
}
4. Exception filters com when
Você já viu isso no Módulo 2: when adiciona uma condição extra ao
catch, capturando só quando essa condição também for verdadeira.
try
{
ExecutarProcedure();
}
catch (SqlException ex) when (ex.Number == 50010)
{
Console.WriteLine("Estoque insuficiente");
}
catch (SqlException ex) when (ex.Number == 50001)
{
Console.WriteLine("Valor de transferência inválido");
}
catch (SqlException ex)
{
Console.WriteLine($"Outro erro de banco: {ex.Number}");
}
5. Relançando uma exceção corretamente
try
{
OperacaoArriscada();
}
catch (Exception ex)
{
LogarErro(ex);
throw; // correto — preserva o stack trace original
// throw ex; // ERRADO — reseta o stack trace, escondendo onde o erro realmente ocorreu
}
Isso é exatamente o mesmo cuidado do THROW; sem argumentos que você
viu no capítulo 6 do Módulo 2 — em ambas as linguagens, relançar "do jeito errado" reconstrói o
erro e perde informação de onde ele realmente aconteceu.
6. Não engula exceções
try
{
ProcessarPagamento(pedido);
}
catch (Exception)
{
// vazio — o erro desaparece silenciosamente
}
Um catch vazio faz a falha desaparecer sem rastro — o pagamento não
foi processado, ninguém sabe, e o sistema segue como se nada tivesse acontecido. Se realmente
não há nada a fazer com um erro específico, ao menos registre-o (log) antes de decidir seguir em
frente.
7. Onde isso te leva
Com tratamento de erro sólido, o próximo capítulo muda de assunto para LINQ — a ferramenta que você vai usar praticamente todos os dias para consultar coleções em memória (e, mais adiante, também o banco via Entity Framework).