C# Exceções Próprias
O .NET tem exceções para os problemas de qualquer programa: argumento inválido, arquivo que não existe, divisão por zero. Mas a lojinha da Ana tem problemas que só ela tem: "o estoque não dá para este pedido", "este produto não está cadastrado". Nesta aula você vai criar as suas próprias classes de exceção, com nome, mensagem e dados que fazem sentido no seu programa, e organizar várias delas numa família.
- Criar uma exceção: uma classe que herda de
Exceptione passa a mensagem parabase. - As regras de nome e de herança que todo programador C# segue.
- Guardar dados extras (o produto, a quantidade) em propriedades da exceção.
- Os três construtores de costume, inclusive o que recebe a
InnerException. - Montar uma família: uma exceção mãe do programa e as filhas, capturadas juntas.
- Quando vale a pena criar uma exceção, e quando uma do .NET basta.
- Os erros
CS0029,CS0155,CS1729eCS8864.
Esta aula usa herança (: Exception), construtores e base. Todos os programas rodaram na máquina da Ana, com o .NET 10 (C# 14), usando dotnet run arquivo.cs na pasta ~/cs.
A ideia antes do código
Num hospital, "deu um problema" não ajuda ninguém. Os avisos têm nome: "código azul", "código vermelho". Cada nome diz o que aconteceu e chama a equipe certa. Quem é da cardiologia corre para o código azul e ignora os outros.
Uma exceção própria é um aviso com nome. Em vez de lançar uma InvalidOperationException genérica para "estoque insuficiente", a lojinha lança uma EstoqueInsuficienteException. Quem chama pode capturar exatamente esse problema com catch (EstoqueInsuficienteException), sem pegar sem querer os outros.
1. A menor exceção possível
Uma exceção é uma classe como outra qualquer, com uma exigência: ela tem de ser da família Exception.
Vender("caderno", 5, 2);
void Vender(string produto, int pedido, int emEstoque)
{
if (pedido > emEstoque)
{
throw new EstoqueInsuficienteException($"Pedido de {pedido} {produto}(s), mas só há {emEstoque}.");
}
Console.WriteLine("Vendido!");
}
class EstoqueInsuficienteException : Exception
{
public EstoqueInsuficienteException(string mensagem) : base(mensagem)
{
}
}
$ dotnet run propria.cs Unhandled exception. EstoqueInsuficienteException: Pedido de 5 caderno(s), mas só há 2. at Program.<<Main>$>g__Vender|0_0(String produto, Int32 pedido, Int32 emEstoque) in /home/ana/cs/propria.cs:line 7 at Program.<Main>$(String[] args) in /home/ana/cs/propria.cs:line 1 $ echo $? 134
- class EstoqueInsuficienteException
- O nome da exceção. Por convenção, sempre termina em
Exception. O compilador não obriga, mas todo mundo espera. - : Exception
- Herda de
Exception. É isso que permite usar a classe comthrowecatch, e que dá a ela, de graça, aMessage, o stack trace, aInnerException… - public EstoqueInsuficienteException(string mensagem)
- Um construtor que recebe a mensagem.
- : base(mensagem)
- Repassa a mensagem para o construtor da classe mãe,
Exception, que a guarda emMessage(aula base). As chaves ficam vazias: não há mais nada a fazer. - Unhandled exception. EstoqueInsuficienteException: …
- No relatório, a sua exceção aparece igual às do .NET. Sem o
System.na frente, porque ela não está em namespace nenhum (veja a seção 4).
| Regra | Por quê |
|---|---|
O nome termina em Exception | Quem lê catch (EstoqueInsuficienteException) sabe na hora o que é. É o padrão de todas as exceções do .NET. |
Herde de Exception (ou de uma exceção sua) | Não use ApplicationException: a própria Microsoft desistiu dela. E não herde de SystemException, que é das exceções do .NET. |
| A mensagem é uma frase completa | Ela aparece no relatório e nos logs. "Pedido de 5 caderno(s), mas só há 2." ajuda; "erro estoque" não. |
A classe fica public num projeto | Quem usa o seu código precisa enxergá-la para escrever o catch. |
Missão: o programa lança uma ProdutoEsgotadoException, mas a classe não existe e o código não compila (CS0246). Crie a classe no fim do arquivo, herdando de Exception, com um construtor que recebe a mensagem e a repassa para base.
int caderno = 0;
if (caderno == 0)
{
throw new ProdutoEsgotadoException("O caderno esgotou.");
}
Console.WriteLine("Ainda tem caderno.");
int caderno = 0;
if (caderno == 0)
{
throw new ProdutoEsgotadoException("O caderno esgotou.");
}
Console.WriteLine("Ainda tem caderno.");
class ProdutoEsgotadoException : Exception
{
public ProdutoEsgotadoException(string mensagem) : base(mensagem)
{
}
}
2. Dados extras: o que o catch precisa saber
A grande vantagem de ter a sua classe é poder guardar dados nela. Quem captura a exceção não precisa "ler" a mensagem para descobrir quantas unidades havia: a informação está numa propriedade.
try
{
Loja.Vender("caderno", 5);
}
catch (EstoqueInsuficienteException e)
{
Console.WriteLine(e.Message);
Console.WriteLine($"Que tal levar só {e.Disponivel}?");
}
static class Loja
{
static readonly Dictionary<string, int> estoque = new() { ["caderno"] = 2, ["caneta"] = 40 };
public static void Vender(string produto, int pedido)
{
int disponivel = estoque[produto];
if (pedido > disponivel)
{
throw new EstoqueInsuficienteException(produto, pedido, disponivel);
}
estoque[produto] = disponivel - pedido;
Console.WriteLine($"Vendido: {pedido} {produto}(s)");
}
}
class EstoqueInsuficienteException : Exception
{
public string Produto { get; }
public int Pedido { get; }
public int Disponivel { get; }
public EstoqueInsuficienteException(string produto, int pedido, int disponivel)
: base($"Pedido de {pedido} {produto}(s), mas só há {disponivel}.")
{
Produto = produto;
Pedido = pedido;
Disponivel = disponivel;
}
}
$ dotnet run dados.cs Pedido de 5 caderno(s), mas só há 2. Que tal levar só 2?
- public int Disponivel { get; }
- Uma propriedade só de leitura (aula readonly e init): quem captura lê, mas não muda. Exceções carregam fatos sobre o que aconteceu; não faz sentido alterá-los depois.
- EstoqueInsuficienteException(string produto, int pedido, int disponivel)
- Agora o construtor recebe os dados, não a mensagem pronta. Quem lança só diz o que aconteceu:
new EstoqueInsuficienteException(produto, pedido, disponivel). - : base($"Pedido de {pedido} …")
- A exceção monta a própria mensagem a partir dos dados. Assim, todas as mensagens desse problema saem no mesmo formato, em qualquer lugar do programa.
- e.Disponivel
- No
catch, a informação vem pronta, como número. Dá para oferecer uma alternativa ao cliente, sem precisar "garimpar" o 2 de dentro do texto.
A forma curta, com construtor primário C# 12
Com um construtor primário, os parâmetros vão direto no nome da classe e a chamada a base vai junto do : Exception(…):
throw new ProdutoEsgotadoException("caderno");
class ProdutoEsgotadoException(string produto) : Exception($"O produto \"{produto}\" esgotou.")
{
public string Produto { get; } = produto;
}
$ dotnet run prim.cs Unhandled exception. ProdutoEsgotadoException: O produto "caderno" esgotou. at Program.<Main>$(String[] args) in /home/ana/cs/prim.cs:line 1
É o mesmo resultado, em menos linhas. O guia usa a forma longa na maior parte das aulas, porque ela deixa cada passo à vista; as duas estão certas.
record
Uma exceção parece um bom caso para record (só dados, só leitura), mas records só herdam de outros records, e Exception é uma classe. record ProdutoEsgotadoException(string Mensagem) : Exception(Mensagem); dá o erro CS8864 (veja Quando dá errado). Use class.
Missão: o programa quebra com a exceção da lojinha. Envolva a chamada da primeira linha num try com catch (EstoqueInsuficienteException e) e mostre Que tal levar só 2? usando a propriedade e.Disponivel (não escreva o 2 à mão).
Loja.Vender("caderno", 5);
static class Loja
{
static readonly Dictionary<string, int> estoque = new() { ["caderno"] = 2, ["caneta"] = 40 };
public static void Vender(string produto, int pedido)
{
int disponivel = estoque[produto];
if (pedido > disponivel)
{
throw new EstoqueInsuficienteException(produto, pedido, disponivel);
}
estoque[produto] = disponivel - pedido;
Console.WriteLine($"Vendido: {pedido} {produto}(s)");
}
}
class EstoqueInsuficienteException : Exception
{
public string Produto { get; }
public int Pedido { get; }
public int Disponivel { get; }
public EstoqueInsuficienteException(string produto, int pedido, int disponivel)
: base($"Pedido de {pedido} {produto}(s), mas só há {disponivel}.")
{
Produto = produto;
Pedido = pedido;
Disponivel = disponivel;
}
}
try
{
Loja.Vender("caderno", 5);
}
catch (EstoqueInsuficienteException e)
{
Console.WriteLine(e.Message);
Console.WriteLine($"Que tal levar só {e.Disponivel}?");
}
static class Loja
{
static readonly Dictionary<string, int> estoque = new() { ["caderno"] = 2, ["caneta"] = 40 };
public static void Vender(string produto, int pedido)
{
int disponivel = estoque[produto];
if (pedido > disponivel)
{
throw new EstoqueInsuficienteException(produto, pedido, disponivel);
}
estoque[produto] = disponivel - pedido;
Console.WriteLine($"Vendido: {pedido} {produto}(s)");
}
}
class EstoqueInsuficienteException : Exception
{
public string Produto { get; }
public int Pedido { get; }
public int Disponivel { get; }
public EstoqueInsuficienteException(string produto, int pedido, int disponivel)
: base($"Pedido de {pedido} {produto}(s), mas só há {disponivel}.")
{
Produto = produto;
Pedido = pedido;
Disponivel = disponivel;
}
}
3. Os três construtores de costume
As exceções do .NET têm, quase todas, três construtores: sem nada, com a mensagem, e com a mensagem e a exceção interna (a InnerException, da aula throw). Numa biblioteca que outras pessoas vão usar, é costume oferecer os mesmos três:
public class LojinhaException : Exception
{
public LojinhaException() { }
public LojinhaException(string mensagem) : base(mensagem) { }
public LojinhaException(string mensagem, Exception interna) : base(mensagem, interna) { }
}
- LojinhaException() { }
- Sem argumentos. A mensagem fica a padrão do .NET (veja abaixo).
- LojinhaException(string mensagem) : base(mensagem)
- O mais usado: só a mensagem.
- LojinhaException(string mensagem, Exception interna) : base(mensagem, interna)
- Para embrulhar uma exceção de baixo nível (um
FormatException, umIOException) numa da lojinha, sem perder a original.
Se você não escrever construtor nenhum, a classe ganha só o construtor vazio, e a mensagem fica a genérica do .NET:
try
{
throw new ProdutoEsgotadoException();
}
catch (ProdutoEsgotadoException e)
{
Console.WriteLine(e.Message);
}
class ProdutoEsgotadoException : Exception
{
}
$ dotnet run semnome.cs Exception of type 'ProdutoEsgotadoException' was thrown.
"Uma exceção do tipo ProdutoEsgotadoException foi lançada." Funciona, mas não diz qual produto. E, nessa classe sem construtores, new ProdutoEsgotadoException("…") nem compila (CS1729): construtores não são herdados em C#; cada classe declara os seus.
4. Uma família de exceções
Um programa maior tem vários problemas próprios. O costume é criar uma exceção mãe do programa, e as outras como filhas dela. Assim, quem chama escolhe: capturar um problema específico, ou capturar "qualquer problema da lojinha" de uma vez. E, num projeto, elas moram no namespace do programa:
using Lojinha;
string[] pedidos = ["caneta:3", "caderno:5", "borracha:1"];
foreach (string p in pedidos)
{
string[] partes = p.Split(':');
try
{
Loja.Vender(partes[0], int.Parse(partes[1]));
}
catch (LojinhaException e)
{
Console.WriteLine($"Não deu: {e.Message} ({e.GetType().Name})");
}
}
Loja.Vender("lápis", 1);
namespace Lojinha
{
class LojinhaException : Exception
{
public LojinhaException() { }
public LojinhaException(string mensagem) : base(mensagem) { }
public LojinhaException(string mensagem, Exception interna) : base(mensagem, interna) { }
}
class EstoqueInsuficienteException : LojinhaException
{
public EstoqueInsuficienteException(string produto, int disponivel)
: base($"só há {disponivel} {produto}(s)") { }
}
class ProdutoNaoEncontradoException : LojinhaException
{
public ProdutoNaoEncontradoException(string produto)
: base($"\"{produto}\" não está cadastrado") { }
}
static class Loja
{
static readonly Dictionary<string, int> estoque = new() { ["caderno"] = 2, ["caneta"] = 40 };
public static void Vender(string produto, int pedido)
{
if (!estoque.TryGetValue(produto, out int disponivel))
{
throw new ProdutoNaoEncontradoException(produto);
}
if (pedido > disponivel)
{
throw new EstoqueInsuficienteException(produto, disponivel);
}
estoque[produto] = disponivel - pedido;
Console.WriteLine($"Vendido: {pedido} {produto}(s)");
}
}
}
$ dotnet run familia.cs Vendido: 3 caneta(s) Não deu: só há 2 caderno(s) (EstoqueInsuficienteException) Não deu: "borracha" não está cadastrado (ProdutoNaoEncontradoException) Unhandled exception. Lojinha.ProdutoNaoEncontradoException: "lápis" não está cadastrado at Lojinha.Loja.Vender(String produto, Int32 pedido) in /home/ana/cs/familia.cs:line 47 at Program.<Main>$(String[] args) in /home/ana/cs/familia.cs:line 16
- class EstoqueInsuficienteException : LojinhaException
- Herda da exceção da lojinha, que herda de
Exception. A árvore fica:Exception→LojinhaException→EstoqueInsuficienteException/ProdutoNaoEncontradoException. - : base($"só há …")
- Chama o construtor
LojinhaException(string mensagem), que por sua vez chama o deException. - catch (LojinhaException e)
- Pega as duas filhas (lembre: o
catchda mãe pega as filhas). Oe.GetType().Namemostra qual delas veio. - Lojinha.ProdutoNaoEncontradoException
- Agora, dentro do namespace, o relatório mostra o nome completo, como o
System.FormatExceptiondo .NET. O"lápis"foi vendido fora dotryde propósito, para você ver esse relatório.
Repare que um FormatException do int.Parse não seria pego pelo catch (LojinhaException e): ele não é da família. É justamente essa a vantagem: o catch pega os problemas da lojinha, que ele sabe tratar, e deixa os defeitos de programação aparecerem.
Criar a sua ou usar uma do .NET?
| Situação | Use |
|---|---|
Um parâmetro null, fora do intervalo, inválido | As do .NET: ArgumentNullException, ArgumentOutOfRangeException, ArgumentException |
| O objeto não está pronto para a operação | InvalidOperationException |
| Um problema do negócio que quem chama vai querer tratar à parte (estoque, saldo, cadastro) | Uma exceção sua: EstoqueInsuficienteException |
| Quem trata precisa de dados (quanto havia, qual produto) | Uma exceção sua, com propriedades |
| Vários problemas próprios, às vezes tratados juntos | Uma família: LojinhaException e filhas |
| Só para ter uma mensagem diferente | Não precisa de classe: throw new InvalidOperationException("…") |
Uma regra prática: crie uma exceção quando você consegue imaginar alguém escrevendo um catch só para ela. Se ninguém nunca vai capturá-la separada, uma do .NET basta.
Quando dá errado
: Exception
throw new ProdutoEsgotado("caderno");
class ProdutoEsgotado
{
public ProdutoEsgotado(string nome) { }
}
$ dotnet run naoexc.cs /home/ana/cs/naoexc.cs(1,7): error CS0029: Não é possível converter implicitamente tipo "ProdutoEsgotado" em "System.Exception" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: o throw só aceita objetos da família Exception, e ProdutoEsgotado não é.
Como consertar: faça a classe herdar de Exception, passe a mensagem para base e, de quebra, ponha o Exception no fim do nome: class ProdutoEsgotadoException : Exception.
catch de algo que não é exceção
try { Console.WriteLine("oi"); }
catch (string e) { }
$ dotnet run catchnao.cs /home/ana/cs/catchnao.cs(2,8): error CS0155: O tipo caught ou thrown deve ser derivado de System.Exception /home/ana/cs/catchnao.cs(2,15): warning CS0168: A variável "e" está declarada, mas nunca é usada Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: "o tipo capturado (caught) ou lançado (thrown) deve ser derivado de System.Exception". A tradução deixou duas palavras em inglês, mas a ideia é a mesma do erro anterior, do lado do catch.
Como consertar: no catch, só tipos da família Exception.
throw new ProdutoEsgotadoException("O caderno esgotou.");
class ProdutoEsgotadoException : Exception
{
}
$ dotnet run semctor.cs /home/ana/cs/semctor.cs(1,11): error CS1729: "ProdutoEsgotadoException" não contém um construtor que aceita 1 argumentos Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: Exception tem um construtor com mensagem, mas a sua classe não herda construtores. Sem nenhum escrito, ela só tem o vazio.
Como consertar: escreva o construtor: public ProdutoEsgotadoException(string mensagem) : base(mensagem) { }.
record
throw new ProdutoEsgotadoException("O caderno esgotou.");
record ProdutoEsgotadoException(string Mensagem) : Exception(Mensagem);
$ dotnet run rec.cs /home/ana/cs/rec.cs(3,52): error CS8864: Os registros só podem ser herdados de um objeto ou de outro registro /home/ana/cs/rec.cs(3,8): error CS0115: "ProdutoEsgotadoException.EqualityContract": não encontrado nenhum método adequado para substituição /home/ana/cs/rec.cs(3,8): error CS0115: "ProdutoEsgotadoException.Equals(Exception?)": não encontrado nenhum método adequado para substituição /home/ana/cs/rec.cs(3,8): error CS0115: "ProdutoEsgotadoException.PrintMembers(StringBuilder)": não encontrado nenhum método adequado para substituição Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: o primeiro erro é o que importa: "registros (records) só podem herdar de object ou de outro record". Os três CS0115 são consequência: o compilador tentou gerar os métodos de um record em cima de uma classe que não é record.
Como consertar: use class, com construtor normal ou primário (a forma curta da seção 2).
O que significa: o construtor da sua exceção não repassou a mensagem para base, ou a exceção foi criada com new …() sem argumentos.
Como consertar: confira se o construtor tem : base(mensagem) (e não só guarda a mensagem numa propriedade sua).
Referência
| Sintaxe | O que faz |
|---|---|
class XException : Exception | Cria uma exceção própria |
public XException(string m) : base(m) { } | Construtor que repassa a mensagem para Exception |
public XException(string m, Exception i) : base(m, i) { } | Construtor que guarda a InnerException |
class XException(string p) : Exception($"… {p}") | Forma curta, com construtor primário C# 12 |
public int Disponivel { get; } | Dado extra, só de leitura |
class FilhaException : MaeException | Família de exceções; catch (MaeException) pega as filhas |
Exception of type 'X' was thrown. | Mensagem padrão, quando nenhuma foi passada |
CS0029 / CS0155 | throw / catch de um tipo que não é Exception |
CS1729 | Falta o construtor com esses argumentos |
CS8864 | Record não pode herdar de Exception |
Exercícios
1. A classe mãe
Complete para criar uma exceção própria:
2. Repassar a mensagem
Complete o construtor:
3. O nome de costume
Uma exceção para "cliente bloqueado" deve se chamar ClienteBloqueado e terminar com:
4. ProdutoNaoEncontradoException herda de LojinhaException, que herda de Exception. Quais catch pegam uma ProdutoNaoEncontradoException?
5. Por que guardar Disponivel numa propriedade em vez de só escrevê-lo na mensagem?
6. Em qual caso não vale a pena criar uma exceção própria?
Missão final: a ProdutoNaoEncontradoException herda direto de Exception, então o catch (LojinhaException e) não a pega e o programa quebra na borracha. Mude só a classe mãe dela, para que o mesmo catch trate os dois problemas da lojinha.
string[] pedidos = ["caneta:3", "caderno:5", "borracha:1"];
foreach (string p in pedidos)
{
string[] partes = p.Split(':');
try
{
Loja.Vender(partes[0], int.Parse(partes[1]));
}
catch (LojinhaException e)
{
Console.WriteLine($"Não deu: {e.Message}");
}
}
class LojinhaException : Exception
{
public LojinhaException(string mensagem) : base(mensagem) { }
}
class EstoqueInsuficienteException : LojinhaException
{
public EstoqueInsuficienteException(string produto, int disponivel)
: base($"só há {disponivel} {produto}(s)") { }
}
class ProdutoNaoEncontradoException : Exception
{
public ProdutoNaoEncontradoException(string produto)
: base($"\"{produto}\" não está cadastrado") { }
}
static class Loja
{
static readonly Dictionary<string, int> estoque = new() { ["caderno"] = 2, ["caneta"] = 40 };
public static void Vender(string produto, int pedido)
{
if (!estoque.TryGetValue(produto, out int disponivel))
{
throw new ProdutoNaoEncontradoException(produto);
}
if (pedido > disponivel)
{
throw new EstoqueInsuficienteException(produto, disponivel);
}
estoque[produto] = disponivel - pedido;
Console.WriteLine($"Vendido: {pedido} {produto}(s)");
}
}
string[] pedidos = ["caneta:3", "caderno:5", "borracha:1"];
foreach (string p in pedidos)
{
string[] partes = p.Split(':');
try
{
Loja.Vender(partes[0], int.Parse(partes[1]));
}
catch (LojinhaException e)
{
Console.WriteLine($"Não deu: {e.Message}");
}
}
class LojinhaException : Exception
{
public LojinhaException(string mensagem) : base(mensagem) { }
}
class EstoqueInsuficienteException : LojinhaException
{
public EstoqueInsuficienteException(string produto, int disponivel)
: base($"só há {disponivel} {produto}(s)") { }
}
class ProdutoNaoEncontradoException : LojinhaException
{
public ProdutoNaoEncontradoException(string produto)
: base($"\"{produto}\" não está cadastrado") { }
}
static class Loja
{
static readonly Dictionary<string, int> estoque = new() { ["caderno"] = 2, ["caneta"] = 40 };
public static void Vender(string produto, int pedido)
{
if (!estoque.TryGetValue(produto, out int disponivel))
{
throw new ProdutoNaoEncontradoException(produto);
}
if (pedido > disponivel)
{
throw new EstoqueInsuficienteException(produto, disponivel);
}
estoque[produto] = disponivel - pedido;
Console.WriteLine($"Vendido: {pedido} {produto}(s)");
}
}
Resumo
- Uma exceção própria é uma classe que herda de
Exceptione tem o nome terminado emException. - O construtor repassa a mensagem com
: base(mensagem); sem isso, a mensagem é "Exception of type '…' was thrown.". Construtores não são herdados (CS1729). - Dados extras vão em propriedades só de leitura; a exceção pode montar a própria mensagem a partir deles.
- Os três construtores de costume: vazio, com mensagem e com mensagem +
InnerException. - Uma família (
LojinhaExceptione filhas) deixa quem chama escolher entre um problema específico e todos os do programa. - Crie uma exceção quando alguém vai querer um
catchsó para ela; para parâmetros inválidos, use as do .NET. Exceção éclass, nuncarecord(CS8864).