Exceções: Tratando o Inesperado

Exceções: Tratando o Inesperado

Como tratar o inesperado sem derrubar o programa: try e catch, a captura de tipos específicos de exceção, e o bloco finally que sempre executa. A aula ensina também a lançar exceções próprias e defende uma filosofia central — falhe claramente, apontando o defeito onde ele nasce.
Linguagem C#

• • 13 min de leitura

Todo programa, por mais bem escrito, encontra o inesperado. O usuário digita uma letra onde se espera um número. Um arquivo que deveria existir foi apagado. Uma divisão acaba tendo zero no denominador. Uma conexão de rede cai no meio da operação. A questão não é se algo dará errado — dará —, mas como seu programa reage quando isso acontece. Um programa frágil simplesmente quebra, encerrando-se abruptamente com uma mensagem técnica incompreensível ao usuário. Um programa robusto antecipa o inesperado e o trata com elegância, continuando a funcionar ou falhando de forma controlada e clara. Nesta aula, que abre a fase da robustez, aprenderemos o mecanismo do C# para lidar com erros durante a execução: as exceções. Você já as viu de relance — era uma exceção que quebrava seu programa quando int.Parse recebia um texto inválido —, e agora aprenderá a dominá-las.

O que é uma exceção

Uma exceção é um objeto que representa um erro ocorrido durante a execução do programa. Quando algo dá errado — uma divisão por zero, uma conversão impossível, um acesso a um arquivo inexistente —, o C# cria um objeto de exceção descrevendo o problema e interrompe o fluxo normal do programa naquele ponto. A partir daí, o programa procura, subindo pela cadeia de chamadas, alguém preparado para lidar com aquele erro. Se ninguém estiver preparado, o programa encerra com uma mensagem de erro — foi o que você viu acontecer com o int.Parse.

O termo "exceção" é apropriado: ela representa uma situação excepcional, fora do fluxo normal esperado. Note a diferença de filosofia em relação a apenas retornar um valor de erro: quando um erro é sinalizado por uma exceção, ele não pode ser ignorado silenciosamente. Ou alguém o trata explicitamente, ou o programa para. Não há como o erro "passar batido" e o programa continuar como se nada tivesse acontecido, corrompendo dados ou produzindo resultados errados sem aviso. Essa característica — a de forçar o reconhecimento do erro — é uma das virtudes das exceções.

Capturando exceções: try e catch

Para tratar uma exceção em vez de deixar o programa quebrar, envolvemos o código arriscado num bloco try (em inglês, "tentar") e fornecemos um bloco catch (em inglês, "capturar") que reage caso um erro ocorra:

try
{
    Console.Write("Digite um número: ");
    int numero = int.Parse(Console.ReadLine());   // pode lançar exceção
    Console.WriteLine($"O dobro é {numero * 2}");
}
catch (Exception erro)
{
    Console.WriteLine("Isso não parece um número válido.");
}

Acompanhe o fluxo. O código dentro do try é executado normalmente. Se nenhuma exceção ocorrer (o usuário digitou um número válido), o bloco catch é ignorado por completo, e tudo segue bem. Mas se uma exceção for lançada dentro do try (o usuário digitou "abc", e int.Parse falhou), a execução do try é imediatamente interrompida naquele ponto — a linha do dobro não roda — e o programa salta para o bloco catch, que trata o erro exibindo uma mensagem amigável. O programa não quebra; ele reage e continua. O objeto de exceção capturado (aqui chamado erro, do tipo Exception) carrega informações sobre o que deu errado — por exemplo, erro.Message traz uma descrição do problema. A palavra Exception no catch significa "capture qualquer tipo de erro"; veremos que se pode ser mais específico.

Compare isso com a situação que enfrentamos na Aula #08. Lá, resolvemos o problema do int.Parse que quebra usando int.TryParse, que evita a exceção. Agora você conhece a outra abordagem: deixar a exceção acontecer e capturá-la com try/catch. Ambas são válidas, e cada uma tem seu lugar. O TryParse é preferível para validação rotineira de entrada (é mais simples e mais eficiente para o caso comum). O try/catch é a ferramenta geral para tratar erros que não têm uma versão "Try", ou situações verdadeiramente excepcionais — como falhas ao acessar arquivos ou recursos externos, onde não há um "TryAbrirArquivo" conveniente.

Tratando erros específicos

Nem todos os erros são iguais, e frequentemente queremos reagir de formas diferentes a tipos diferentes de erro. O C# permite capturar tipos específicos de exceção, fornecendo múltiplos blocos catch, do mais específico ao mais geral:

try
{
    Console.Write("Numerador: ");
    int a = int.Parse(Console.ReadLine());
    Console.Write("Denominador: ");
    int b = int.Parse(Console.ReadLine());
    Console.WriteLine($"Resultado: {a / b}");
}
catch (DivideByZeroException erro)   // erro específico: divisão por zero
{
    Console.WriteLine("Não é possível dividir por zero.");
}
catch (FormatException erro)         // erro específico: formato inválido
{
    Console.WriteLine("Você deve digitar apenas números.");
}
catch (Exception erro)               // qualquer outro erro (rede de segurança)
{
    Console.WriteLine($"Ocorreu um erro inesperado: {erro.Message}");
}

Cada catch trata um tipo de erro. DivideByZeroException captura especificamente a divisão por zero; FormatException captura a conversão de um texto inválido; e o catch (Exception) final serve como rede de segurança para qualquer erro não previsto pelos anteriores. Um detalhe importante da ordem: os catch são testados de cima para baixo, e o primeiro que corresponde ao tipo do erro é usado. Por isso, o catch (Exception) — que captura tudo — deve vir por último: se viesse antes, capturaria todos os erros e os catch específicos nunca seriam alcançados. A regra: do mais específico ao mais geral, sempre nessa ordem. Reagir especificamente a cada tipo de erro permite mensagens mais úteis e tratamentos mais adequados do que uma resposta genérica a tudo.

O bloco finally: o que sempre acontece

Há um terceiro bloco, o finally (em inglês, "finalmente"), que executa sempre — quer o try termine normalmente, quer uma exceção seja lançada e capturada. Ele é o lugar para ações de "limpeza" que precisam acontecer independentemente do que ocorreu, como fechar um arquivo ou liberar um recurso:

try
{
    Console.WriteLine("Iniciando operação...");
    // ... código que pode ou não lançar exceção ...
}
catch (Exception erro)
{
    Console.WriteLine($"Algo deu errado: {erro.Message}");
}
finally
{
    Console.WriteLine("Encerrando (isto sempre executa).");
}

A mensagem do finally aparece sempre, com ou sem erro. Isso é valioso quando há algo que precisa ser feito ao final — tipicamente liberar um recurso externo, como um arquivo aberto — de modo que essa liberação aconteça mesmo que uma exceção interrompa o fluxo no meio. Sem o finally, se uma exceção ocorresse antes da liberação, o recurso poderia ficar preso. O finally garante que a limpeza ocorra em qualquer cenário.

Lançando suas próprias exceções

Até aqui, tratamos exceções que o C# lançava. Mas você também pode lançar uma exceção deliberadamente, quando seu próprio código detecta uma situação que não pode ou não deve tratar ali mesmo. Usamos a palavra throw (em inglês, "lançar"), com um objeto de exceção apropriado:

void DefinirIdade(int idade)
{
    if (idade < 0)
    {
        // Situação inválida: lançamos uma exceção em vez de aceitar valor absurdo.
        throw new ArgumentException("A idade não pode ser negativa.");
    }
    Console.WriteLine($"Idade definida: {idade}");
}

Aqui, ao receber uma idade negativa, o método lança uma ArgumentException (uma exceção padrão que indica "argumento inválido"), com uma mensagem descritiva. Quem chamar esse método com uma idade negativa receberá a exceção, que poderá tratar com try/catch ou deixar subir. Lançar exceções é útil para sinalizar, de forma inequívoca, condições que violam as regras do seu código — situações que o método não sabe (ou não deve) resolver sozinho, e que precisam ser tratadas por quem o chamou.

Uma filosofia: falhe claramente, não em silêncio

Encerro com um princípio valioso, no espírito de honestidade do curso. Um erro que estoura visivelmente é quase sempre melhor do que um erro engolido em silêncio. Existe um anti-padrão tentador para o iniciante: capturar uma exceção e não fazer nada com ela, deixando o programa continuar como se nada tivesse acontecido:

try { /* código arriscado */ }
catch (Exception) { }   // NUNCA faça isto: o erro desaparece, mas o problema continua

Esse catch vazio é perigoso: ele mascara o problema, deixando o programa seguir num estado possivelmente corrompido, até quebrar bem mais tarde, num ponto distante e sem pistas da causa real — um pesadelo para depurar. A regra: só capture uma exceção se você tem algo útil a fazer com ela — tratá-la de verdade, avisar o usuário, tentar uma alternativa, ou ao menos registrar que ocorreu. Se não tem o que fazer, muitas vezes é melhor deixar o erro subir e falhar claramente, com uma mensagem que aponte a causa. Errar alto e cedo é uma dádiva para quem depura; errar baixo e tarde é um tormento. Trate as exceções como sinais valiosos, não como incômodos a silenciar.

O inesperado ganhou tratamento em vez de derrubar o programa. Com try e catch, uma operação arriscada passa a ter um plano alternativo; capturar tipos específicos permite reagir de forma adequada a cada situação; e finally garante a execução do que precisa acontecer de todo jeito, tenha dado certo ou errado.

Lançar exceções próprias completou o quadro e trouxe junto uma filosofia que vale carregar: falhe claramente, nunca em silêncio. Um método que recebe um argumento inválido e devolve um resultado qualquer esconde o defeito e o empurra para longe da causa; um método que recusa o argumento com uma exceção clara aponta o problema exatamente onde ele nasceu.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Escreva um programa que peça um número ao usuário e use int.Parse dentro de um bloco try, com um catch que exiba uma mensagem amigável se a conversão falhar. Teste com uma entrada válida e uma inválida, confirmando que o programa não quebra na segunda.

Ver resposta

✓ Resposta: Exemplo:

try
{
    Console.Write("Número: ");
    int n = int.Parse(Console.ReadLine());
    Console.WriteLine($"Dobro: {n * 2}");
}
catch (Exception erro)
{
    Console.WriteLine("Entrada inválida — digite um número.");
}

Com entrada válida, o dobro é exibido; com entrada inválida como "abc", o int.Parse lança uma exceção que é capturada pelo catch, exibindo a mensagem amigável — o programa não quebra, ao contrário do que aconteceria sem o try/catch.

Exercício 2

Escreva um programa que peça dois números e exiba a divisão do primeiro pelo segundo, com blocos catch separados para DivideByZeroException (denominador zero) e FormatException (entrada não numérica), mais um catch (Exception) geral ao final. Teste os três cenários e explique por que o catch geral deve vir por último.

Ver resposta

✓ Resposta: Exemplo:

try
{
    int a = int.Parse(Console.ReadLine());
    int b = int.Parse(Console.ReadLine());
    Console.WriteLine(a / b);
}
catch (DivideByZeroException) { Console.WriteLine("Divisão por zero!"); }
catch (FormatException) { Console.WriteLine("Digite apenas números."); }
catch (Exception erro) { Console.WriteLine($"Erro: {erro.Message}"); }

O catch geral (Exception) deve vir por último porque os catch são testados de cima para baixo e o primeiro compatível é usado; como Exception captura qualquer erro, se viesse antes, capturaria tudo e os catch específicos (divisão por zero, formato) nunca seriam alcançados. Colocando-o por último, os específicos têm a chance de tratar seus casos, e o geral só recolhe o que sobrar.

Exercício 3

Escreva um programa que use um bloco finally para exibir uma mensagem de "operação encerrada" que apareça tanto quando o código do try funciona quanto quando ele lança uma exceção. Confirme os dois casos e explique a utilidade do finally.

Ver resposta

✓ Resposta: Exemplo:

try
{
    Console.WriteLine("Executando...");
    // troque por código que lance ou não uma exceção para testar os dois casos
}
catch (Exception erro)
{
    Console.WriteLine($"Erro: {erro.Message}");
}
finally
{
    Console.WriteLine("Operação encerrada.");
}

A mensagem "Operação encerrada" aparece tanto quando o try completa sem erro quanto quando uma exceção é lançada e capturada. A utilidade do finally é garantir que certas ações — tipicamente a liberação de recursos, como fechar um arquivo — aconteçam em qualquer cenário, mesmo que uma exceção interrompa o fluxo no meio, evitando que recursos fiquem presos.

Exercício 4

Escreva um método void SacarValor(decimal valor) que lance uma ArgumentException (com throw) se o valor for negativo ou zero, e que exiba uma confirmação caso contrário. Chame-o dentro de um try/catch, testando com um valor válido e um inválido.

Ver resposta

✓ Resposta: Exemplo:

void SacarValor(decimal valor)
{
    if (valor <= 0)
        throw new ArgumentException("O valor do saque deve ser positivo.");
    Console.WriteLine($"Saque de R$ {valor} realizado.");
}

try
{
    SacarValor(100m);   // válido
    SacarValor(-50m);   // inválido: lança exceção
}
catch (ArgumentException erro)
{
    Console.WriteLine($"Erro: {erro.Message}");
}

O primeiro saque é confirmado; o segundo, com valor negativo, faz o método lançar a ArgumentException, capturada pelo catch, que exibe a mensagem. O throw sinaliza a violação da regra de forma inequívoca.

Exercício 5

Sem código: explique por que um catch vazio (que captura a exceção e não faz nada) é considerado um anti-padrão perigoso. Que problema de depuração ele costuma causar, e o que você deveria fazer em vez de simplesmente silenciar o erro?

Ver resposta

✓ Resposta: Um catch vazio é perigoso porque captura a exceção e não faz nada com ela, fazendo o erro desaparecer sem ser tratado nem registrado. O problema de depuração é que o programa continua executando num estado possivelmente corrompido (pois o erro real não foi resolvido) e só quebra bem mais tarde, num ponto distante da causa original e sem pistas — o rastro do erro foi apagado, tornando a investigação muito difícil. Em vez de silenciar o erro, você deveria fazer algo útil: tratá-lo de verdade (por exemplo, tentar uma alternativa), avisar o usuário com uma mensagem clara, registrar que o erro ocorreu, ou — se não há o que fazer ali — deixar a exceção subir para que falhe claramente, com uma mensagem que aponte a causa. Tratar a exceção como um sinal valioso, e não como um incômodo a esconder, é o que permite programas confiáveis e fáceis de depurar.

Comentários

Mais em Linguagem C#

Arrays: Guardando Muitos Valores Juntos
Arrays: Guardando Muitos Valores Juntos

Como guardar muitos valores sob um único nome: o array, uma fileira de…

Validação e Robustez em APIs
Validação e Robustez em APIs

O princípio inegociável de qualquer API: nunca confie na entrada. A aula cobre…

Sua Primeira API REST com ASP.NET Core
Sua Primeira API REST com ASP.NET Core

O primeiro serviço web em C#: uma API REST com as Minimal APIs do ASP.NET…