Capturar exceções é metade do tratamento de erros; a outra metade é lançar a exceção certa. Uma exceção bem escolhida diz a quem chama exatamente o que deu errado e se foi erro de quem chamou ou do estado do programa. Esta página cobre o lado do lançamento; try catch cobre o tratamento.
A instrução throw e as guard clauses
throw recebe um objeto de exceção. A execução do método para ali, e a exceção procura um tratador subindo pela pilha de chamadas. O uso mais comum é uma guard clause: verificações no topo de um método que rejeitam entradas inválidas antes de qualquer trabalho ser feito.
Saída:
ArgumentOutOfRangeException for parameter 'amount'
InvalidOperationException: The account is frozen.
Balance: 100
nameof(amount) produz a string "amount" e continua correto se o parâmetro for renomeado. As exceções de argumento a guardam em ParamName, que ferramentas e logs usam para apontar o argumento inválido.
Guard clauses mantêm o resto do método simples: depois das verificações, o código pode supor que a entrada é válida. Elas também falham no ponto do erro, em vez de deixar um valor inválido seguir adiante e causar uma NullReferenceException confusa três métodos depois.
Qual tipo de exceção lançar
Reutilize um tipo embutido quando ele descreve a situação; quem chama já sabe tratá-lo.
| Situação | Lance |
|---|---|
Um argumento obrigatório é null | ArgumentNullException |
| Um argumento está fora do intervalo permitido (quantidade negativa, índice depois do fim) | ArgumentOutOfRangeException |
| Um argumento é inválido de outra forma (nome vazio, ID malformado) | ArgumentException |
| A chamada não é válida no estado atual do objeto | InvalidOperationException |
A operação nunca é suportada por este tipo (o Add de uma coleção somente leitura) | NotSupportedException |
| O método ainda não foi escrito | NotImplementedException |
Um objeto foi usado depois do Dispose | ObjectDisposedException |
| Uma operação com prazo ficou sem tempo | TimeoutException |
A linha entre as três primeiras linhas e InvalidOperationException é quem precisa mudar alguma coisa. Uma exceção de argumento diz "chame isto de outro jeito". InvalidOperationException diz "a chamada estava certa, mas não agora".
Não lance Exception, SystemException nem ApplicationException diretamente: quem chama não consegue capturá-las sem capturar também todo o resto. Também não lance você mesmo NullReferenceException, IndexOutOfRangeException nem StackOverflowException; o runtime as reserva para bugs de verdade.
Throw expressions
Antes do C# 7, throw era só uma instrução. Desde o C# 7, ele também pode aparecer como expressão em três lugares, o que transforma verificações comuns em uma linha:
Saída:
Ana <ana@example.com>
Null: name
ArgumentException: email
Repare que ArgumentNullException deriva de ArgumentException, então um catch (ArgumentException) colocado primeiro também capturaria o caso de null. A ordem das cláusulas catch importa pelo mesmo motivo quando você trata as duas.
ThrowIfNull e parecidos (.NET 6 em diante)
O .NET moderno adiciona auxiliares static que escrevem a verificação e o lançamento para você, com o nome do parâmetro capturado automaticamente:
public void Ship(Order order, int quantity, string address)
{
ArgumentNullException.ThrowIfNull(order); // .NET 6
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity); // .NET 8
ArgumentException.ThrowIfNullOrWhiteSpace(address); // .NET 8
ObjectDisposedException.ThrowIf(disposed, this); // .NET 7
// ...
}
Eles se comportam igual ao if com throw escrito à mão, e mantêm cada guard clause em uma linha. Em plataformas mais antigas, escreva a forma com if mostrada antes.
Escrevendo uma classe de exceção própria
Crie seu próprio tipo de exceção quando quem chama precisa capturar essa falha específica separadamente, ou quando o tratador precisa de dados que uma string de mensagem não carrega bem.
Saída:
Cannot withdraw 25 from a balance of 15.
Short by 10
As convenções:
- O nome termina em
Exception. - Ela deriva de
Exception(ou de um tipo embutido mais específico quando é um caso especial dele, comoInvalidOperationException). - Ela tem os três construtores padrão: sem argumentos, com mensagem e com mensagem mais inner exception. Adicione seus próprios construtores por cima.
- Dados extras vão em propriedades somente leitura, definidas no construtor. Um tratador pode então agir sobre
e.Requestedem vez de interpretar a mensagem.
Envolvendo com uma inner exception
Quando uma falha de baixo nível deve aparecer como uma de nível mais alto, envolva-a. A original fica guardada como InnerException, então nenhuma informação se perde:
Saída:
Setting 'port' must be a number, got '80a'.
Caused by: FormatException
Quem chama agora lida com termos de configuração, que entende, e registrar e.ToString() imprime a cadeia inteira, incluindo a FormatException e o stack trace dela. Só envolva quando você acrescenta significado; envolver toda exceção em uma MyAppException genérica só obriga os tratadores a cavar pela InnerException.
Lançar versus retornar um resultado
Exceções são para falhas que quem chama não espera na operação normal. Para resultados rotineiros, como uma consulta que muitas vezes não encontra nada ou uma entrada de usuário que muitas vezes é inválida, a convenção do .NET é o padrão Try: retornar bool e devolver o valor por um parâmetro out.
public bool TryWithdraw(decimal amount, out string error)
{
if (amount > Balance) { error = "Insufficient funds."; return false; }
Balance -= amount;
error = null;
return true;
}
Muitos tipos oferecem os dois: int.Parse lança exceção, int.TryParse retorna false; dict[key] lança exceção, dict.TryGetValue retorna false. Lançar uma exceção custa muito mais que retornar um valor, então isso não deve ficar em um caminho que executa milhares de vezes por segundo. Veja ref e out sobre parâmetros out.
Escrevendo boas mensagens
A mensagem de uma exceção é lida por um desenvolvedor olhando um log. Faça-a dizer o que estava errado e, quando for seguro, o valor problemático: "Quantity must be between 1 and 99, got 0." é melhor que "Invalid input.". Escreva frases completas e mantenha segredos como senhas e tokens fora das mensagens, já que elas acabam em arquivos de log.
Erros comuns
- Lançar a própria
Exception. Quem chama não consegue capturá-la de forma seletiva; use um tipo específico. - Passar a mensagem no lugar do nome do parâmetro.
new ArgumentNullException("name")recebe o nome do parâmetro; a mensagem vem em segundo lugar. - Nomes de parâmetro escritos à mão. Use
nameof(param)para que renomeações os mantenham corretos. - Exceções próprias sem significado extra. Se um tipo embutido serve, use-o.
- Perder o erro original ao envolver. Sempre o passe como inner exception.
Perguntas frequentes
Como lançar uma exceção em C#?
Crie um objeto de exceção e lance-o: throw new ArgumentException("Amount must be positive", nameof(amount));. A execução para nessa linha e a exceção sobe pela pilha de chamadas até o catch correspondente mais próximo. Escolha o tipo embutido mais específico que descreve o problema, ou um tipo próprio quando quem chama precisa tratar esse caso separadamente.
Como criar uma exceção personalizada em C#?
Crie uma classe derivada de Exception cujo nome termine em Exception e dê a ela os construtores padrão: um sem argumentos, um que recebe uma mensagem e um que recebe uma mensagem e uma inner exception, cada um chamando o construtor base(...) correspondente. Adicione propriedades somente leitura para os dados de que um tratador precisa, como um ID de pedido ou um saldo.
Quando lançar ArgumentException e quando lançar InvalidOperationException?
Lance uma ArgumentException (ou ArgumentNullException / ArgumentOutOfRangeException) quando quem chama passou um valor inválido: a solução é chamar o método de outro jeito. Lance InvalidOperationException quando os argumentos estão certos, mas o objeto está no estado errado para a chamada, como ler de uma conexão fechada ou sacar de uma conta bloqueada.
O que é uma throw expression em C#?
Desde o C# 7, throw pode ser usado como expressão em três lugares: depois de ??, em qualquer um dos ramos de ?: e como corpo de um membro com corpo de expressão ou de uma lambda. Por exemplo, _name = name ?? throw new ArgumentNullException(nameof(name)); atribui ou lança em uma linha.
O que o ArgumentNullException.ThrowIfNull faz?
É um auxiliar static adicionado no .NET 6: ArgumentNullException.ThrowIfNull(customer); lança ArgumentNullException com o nome do parâmetro preenchido automaticamente quando customer é null, e não faz nada caso contrário. Versões posteriores adicionaram auxiliares parecidos, como ArgumentException.ThrowIfNullOrEmpty (.NET 7) e ArgumentOutOfRangeException.ThrowIfNegative (.NET 8).