Quando algo dá errado em tempo de execução (um arquivo não existe, um texto não é um número, uma chave não está em um dicionário), o .NET lança uma exceção: um objeto que descreve o erro. A exceção sobe pela pilha de chamadas até que um bloco catch a trate. Se nada tratar, o programa para e imprime o erro.
Um try catch básico
Envolva em try o código que pode falhar e trate a falha no catch:
Saída:
42 doubled is 84
'forty-two' is not a number
7 doubled is 14
Still running
Quando int.Parse("forty-two") lança a exceção, o Console.WriteLine depois dele no bloco try é pulado, o bloco catch (FormatException) executa e o laço continua. Você pode omitir a variável (catch (FormatException)) quando não precisa do objeto da exceção.
Esse caso específico tem uma ferramenta melhor: int.TryParse(input, out int n) retorna false em vez de lançar exceção. Exceções são para situações que o código não espera; uma entrada que muitas vezes é inválida é esperada, então verifique-a em vez de capturar.
Como uma exceção sobe
Uma exceção lançada no fundo de uma cadeia de chamadas desenrola todos os métodos até encontrar um tratador correspondente. O código depois do lançamento em cada um desses métodos não executa.
Saída:
OrderTotal finished
5.00
Caught KeyNotFoundException in Main
PriceOf e OrderTotal não têm catch, então a exceção passa direto por eles até o Main. Coloque um tratador no nível que sabe o que fazer com a falha, que muitas vezes não é onde ela aconteceu.
Capturando exceções específicas, na ordem certa
Uma cláusula catch trata o seu tipo e todos os tipos derivados dele. Quando há várias cláusulas, a primeira que corresponde vence, então ordene-as da mais específica para a mais geral:
Saída:
10 / 2 = 5
Cannot divide by zero
Both values must be whole numbers
Unexpected: OverflowException
A última chamada lança OverflowException, que nenhuma das cláusulas específicas trata, então o catch (Exception e) geral trata. Colocar catch (Exception) primeiro tornaria inalcançáveis as cláusulas depois dele, e o compilador rejeita isso com o erro CS0160.
Filtros de exceção com when
O C# 6 adicionou o when: uma condição que decide se uma cláusula catch se aplica. Se ela for false, o runtime continua procurando outro tratador como se a cláusula não existisse.
Saída:
Not found: show an empty page
Server error 503: retry later
Unhandled by Call: HTTP 401
Filtros permitem ramificar com base em dados dentro da exceção sem capturar e relançar. Eles também são a forma limpa de tratar dois tipos sem relação da mesma maneira, como faz a terceira cláusula. Um filtro executa antes de a pilha ser desenrolada, então um depurador ou um crash dump ainda mostra o estado original quando nenhum filtro corresponde.
finally: código que sempre executa
Um bloco finally executa quando o controle sai do try, seja porque ele terminou, retornou antes ou lançou uma exceção. É onde fica a limpeza. (Uma ressalva: se nada em lugar nenhum capturar a exceção, o processo pode terminar sem executá-lo.)
Saída:
Open connection
Close connection
finished
Open connection
Close connection
returned early
Open connection
Close connection
handled error
Em todos os casos, "Close connection" é impresso antes de o resultado do método chegar ao Main: o finally executa depois que o valor do return é calculado, mas antes de o método realmente retornar. Um try pode ter um finally sem nenhum catch, o que faz a limpeza e deixa a exceção seguir para quem chamou.
Para objetos que implementam IDisposable (arquivos, streams, conexões), a instrução using escreve esse try/finally para você.
Relançando: throw; vs throw e;
Às vezes um bloco catch registra ou anota algo e depois deixa a exceção continuar. A forma como você relança decide se o stack trace sobrevive:
Saída:
throw; trace mentions LoadConfig: True
throw e; trace mentions LoadConfig: False
throw e; trata a exceção como se ela tivesse sido lançada agora no bloco catch, então os quadros abaixo dele, incluindo o método onde o erro aconteceu, somem do trace. Sempre relance com um throw; sozinho. (O atributo NoInlining só está ali porque o JIT pode fundir um método tão pequeno com quem o chama, o que o esconderia dos dois traces.)
Para acrescentar contexto, envolva a exceção em uma nova e passe a original como inner exception: throw new ConfigException("Could not start the app", e);. A inner exception mantém o próprio stack trace, e os loggers imprimem a cadeia. Escrever seus próprios tipos de exceção é assunto de lançar exceções.
O objeto Exception
Toda exceção deriva de System.Exception. Os membros que você mais usa:
| Membro | O que guarda |
|---|---|
Message | Uma descrição legível |
GetType().Name | O tipo da exceção, como FormatException |
StackTrace | A cadeia de chamadas de métodos no ponto do lançamento |
InnerException | A exceção que causou esta, ou null |
ToString() | Tipo, mensagem, inner exceptions e stack trace juntos |
Registre e.ToString() em vez de e.Message quando quiser diagnosticar uma falha depois: só a mensagem raramente diz onde estava o problema. Não mostre e.ToString() para os usuários finais.
Tipos de exceção comuns
| Exceção | Causa típica |
|---|---|
NullReferenceException | Chamar um membro em uma referência null |
ArgumentNullException | Um método recebeu null onde precisa de um valor |
ArgumentOutOfRangeException | Um argumento ou índice de lista está fora do intervalo permitido |
IndexOutOfRangeException | Um índice de array está fora dos limites |
FormatException | int.Parse, DateTime.Parse e parecidos em um texto com formato errado |
InvalidCastException | Um cast explícito para um tipo que o objeto não é |
InvalidOperationException | O objeto está no estado errado para a chamada (sequência vazia, coleção modificada) |
KeyNotFoundException | Ler com o indexador uma chave que não está no dicionário |
DivideByZeroException | Divisão de inteiro ou decimal por zero |
OverflowException | Um número convertido, uma conversão checked ou um resultado aritmético checked não cabe no tipo |
FileNotFoundException, IOException | Problemas no sistema de arquivos |
NullReferenceException, IndexOutOfRangeException e InvalidCastException quase sempre indicam um bug. Corrija o código em vez de capturá-las.
Não engolir exceções
Um catch vazio esconde todos os erros, inclusive os que você não previu:
try
{
SaveOrder(order);
}
catch (Exception)
{
// nothing: the order silently was not saved
}
O programa continua executando como se o salvamento tivesse funcionado, e a causa real se perde. Diretrizes que mantêm o tratamento de exceções honesto:
- Capture só o que você consegue tratar, no nível que consegue tratar.
- Se você captura para registrar, relance com
throw;, a menos que o programa realmente possa continuar. - Capture
Exceptionsó na borda externa:Main, um tratador de requisições, um laço de worker. - Prefira
TryParse,TryGetValuee verificações denulla exceções nos casos esperados. Lançar uma exceção é lento comparado a uma verificação, enquanto um blocotryque não lança nada quase não custa.
Erros comuns
catch (Exception)primeiro. As cláusulas seguintes ficam inalcançáveis (CS0160).throw e;para relançar. Apaga o stack trace original; usethrow;.- Blocos catch vazios. Os erros desaparecem; no mínimo registre e relance.
- Usar exceções para controlar o fluxo. Valide a entrada com
TryParseem vez de capturarFormatException. - Mostrar aos usuários o
e.Messagede uma exceção do framework. O texto muda entre versões do .NET e é escrito para desenvolvedores.
Perguntas frequentes
Como o try catch funciona em C#?
O código que pode falhar vai no bloco try. Se uma instrução ali lança uma exceção, o resto do bloco é pulado e o runtime procura uma cláusula catch cujo tipo corresponda à exceção, primeiro no método atual e depois em cada método que o chamou. O primeiro catch correspondente executa, e a execução continua depois da instrução try inteira.
O finally sempre executa em C#?
Quase sempre: depois que o bloco try termina normalmente, depois que um catch trata uma exceção, depois de um return ou break dentro do bloco, e quando uma exceção passa por ali a caminho de um catch mais acima na pilha de chamadas. Ele não executa quando o processo termina antes: um processo encerrado à força, Environment.FailFast, uma StackOverflowException e, no .NET Core em diante, uma exceção que nada captura, que encerra o processo antes de os blocos finally executarem.
Como capturar várias exceções em C#?
Escreva várias cláusulas catch, com o tipo mais específico primeiro: catch (FileNotFoundException) antes de catch (IOException) antes de catch (Exception). O compilador rejeita uma cláusula que nunca pode ser alcançada porque uma anterior já captura o tipo dela. Para tratar dois tipos sem relação da mesma forma, use um filtro: catch (Exception e) when (e is FormatException || e is OverflowException).
Qual a diferença entre throw e throw ex em C#?
Dentro de um catch, throw; relança a exceção atual com o stack trace original. throw ex; lança o mesmo objeto, mas reinicia o stack trace na linha atual, então o método onde o erro realmente aconteceu some do trace. Use throw;, ou envolva a exceção: throw new MyException("context", ex);.
O que é um filtro de exceção em C#?
Uma cláusula when depois de um catch (C# 6 em diante): catch (HttpRequestException e) when (e.Message.Contains("404")). O bloco catch só executa se a condição for true; caso contrário, a exceção continua procurando outro tratador como se a cláusula não existisse, e a pilha não é desenrolada.
Devo capturar Exception em C#?
Só nas bordas de um programa: o topo do Main, um tratador de requisições ou um laço em segundo plano, onde o trabalho é registrar o erro e seguir em frente ou sair de forma limpa. Nas partes internas do código, capture os tipos específicos que você realmente consegue tratar e deixe o resto se propagar.