C# throw

Até agora, quem lançava as exceções era o .NET: o int.Parse, a divisão por zero, o array. Nesta aula é a sua vez. Com a palavra throw ("lançar", "atirar"), o seu método avisa que recebeu algo que não dá para aceitar, do mesmo jeito que os métodos do .NET fazem. Você também vai aprender a relançar uma exceção sem perder o rastro dela (throw; × throw ex;), a embrulhar uma exceção em outra e a usar throw no meio de uma expressão.

Você vai aprender
  • Lançar uma exceção com throw new …, e quem a recebe.
  • Qual exceção do .NET escolher: ArgumentException, ArgumentNullException, ArgumentOutOfRangeException, InvalidOperationException, NotImplementedException…
  • Os atalhos de validação: ArgumentNullException.ThrowIfNull, ArgumentOutOfRangeException.ThrowIfNegative e companhia.
  • Relançar dentro de um catch: por que throw; e não throw ex; (o aviso CA2200).
  • Embrulhar uma exceção em outra com InnerException.
  • As expressões throw: ?? throw, no ternário, no switch e depois de =>.
  • Os erros CS0029, CS0156 e CS8115.

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

A Ana é a gerente da lojinha, e a funcionária nova está no caixa. Um cliente pede 150% de desconto. A funcionária não deve "dar um jeito" (cobrar um valor negativo, inventar um desconto qualquer): ela deve parar e chamar a gerente, dizendo exatamente o problema: "o desconto pedido é 150%, e o máximo é 100%". Quem sabe o que fazer com isso é a gerente.

O throw é esse "chamar a gerente". Um método que recebe um valor impossível lança uma exceção com uma mensagem clara, e quem chamou o método decide: trata com catch, ou deixa subir.


1. Lançar uma exceção: throw new …

Console.WriteLine(Loja.AplicarDesconto(80m, 10));
Console.WriteLine(Loja.AplicarDesconto(80m, 150));
Console.WriteLine("Fim");

static class Loja
{
    public static decimal AplicarDesconto(decimal preco, int percentual)
    {
        if (percentual < 0 || percentual > 100)
        {
            throw new ArgumentOutOfRangeException(nameof(percentual), percentual, "O desconto vai de 0 a 100.");
        }
        return preco - preco * percentual / 100;
    }
}
$ dotnet run desconto.cs
72
Unhandled exception. System.ArgumentOutOfRangeException: O desconto vai de 0 a 100. (Parameter 'percentual')
Actual value was 150.
   at Loja.AplicarDesconto(Decimal preco, Int32 percentual) in /home/ana/cs/desconto.cs:line 11
   at Program.<Main>$(String[] args) in /home/ana/cs/desconto.cs:line 2
$ echo $?
134
if (percentual < 0 || percentual > 100)
A verificação: o desconto só faz sentido de 0 a 100.
throw
"Lance." Interrompe o método na hora, como um return, mas em vez de devolver um valor, lança a exceção para quem chamou. A linha do return não roda.
new ArgumentOutOfRangeException(…)
Cria o objeto da exceção, como qualquer new (aula Classes e Objetos). "Argumento fora do intervalo" é a exceção certa para um número fora dos limites.
nameof(percentual)
O nome do parâmetro, como texto: "percentual". Usar nameof em vez de escrever o texto à mão garante que, se você renomear o parâmetro, a mensagem acompanha. Aparece no relatório como (Parameter 'percentual').
percentual
O valor recebido. Aparece como Actual value was 150. ("o valor recebido foi 150").
"O desconto vai de 0 a 100."
A mensagem. A sua mensagem pode ser em português: ela aparece exatamente como você escreveu.

O relatório é igual aos do .NET: tipo, mensagem e stack trace. A primeira linha at aponta a linha 11, a do throw; a de baixo, a linha 2, de onde veio o 150. Para quem chama, não há diferença entre uma exceção lançada pelo .NET e uma lançada por você: dá para capturar com catch (ArgumentOutOfRangeException e), ler a e.Message etc.

Por que não só mostrar uma mensagem?

O método poderia fazer Console.WriteLine("Desconto inválido") e devolver o preço sem desconto. Mas aí quem chamou nunca fica sabendo que deu errado, e o programa segue com um valor que ninguém pediu. Um método não sabe onde está sendo usado (num programa de console, numa página web, num teste automático) nem o que é melhor fazer em cada caso. O throw entrega o problema a quem sabe.

Missão: o método Vender aceita quantidade negativa, e o estoque "cresce" numa venda. Faça-o lançar uma ArgumentOutOfRangeException quando a quantidade for menor ou igual a zero, com a mensagem A quantidade tem de ser positiva.

int estoque = 10;
estoque = Vender(estoque, 3);
Console.WriteLine($"Estoque: {estoque}");
estoque = Vender(estoque, -5);
Console.WriteLine($"Estoque: {estoque}");

int Vender(int emEstoque, int quantidade)
{
    return emEstoque - quantidade;
}
int estoque = 10;
estoque = Vender(estoque, 3);
Console.WriteLine($"Estoque: {estoque}");
estoque = Vender(estoque, -5);
Console.WriteLine($"Estoque: {estoque}");

int Vender(int emEstoque, int quantidade)
{
    if (quantidade <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(quantidade), quantidade, "A quantidade tem de ser positiva.");
    }
    return emEstoque - quantidade;
}

2. Qual exceção lançar

O .NET já tem exceções prontas para os problemas mais comuns. Prefira sempre uma delas, porque todo programador C# sabe o que cada uma quer dizer:

try
{
    throw new ArgumentException("O nome não pode ficar vazio.");
}
catch (ArgumentException e)
{
    Console.WriteLine(e.Message);
}
try
{
    throw new ArgumentException("O nome não pode ficar vazio.", "nome");
}
catch (ArgumentException e)
{
    Console.WriteLine(e.Message);
    Console.WriteLine(e.ParamName);
}
try
{
    throw new InvalidOperationException("O caixa está fechado.");
}
catch (InvalidOperationException e)
{
    Console.WriteLine(e.Message);
}
try
{
    throw new ArgumentNullException("cliente");
}
catch (ArgumentNullException e)
{
    Console.WriteLine(e.Message);
}
$ dotnet run simples.cs
O nome não pode ficar vazio.
O nome não pode ficar vazio. (Parameter 'nome')
nome
O caixa está fechado.
Value cannot be null. (Parameter 'cliente')

Repare na ordem dos argumentos, que pega muita gente: no ArgumentException, a mensagem vem primeiro e o nome do parâmetro depois; no ArgumentNullException e no ArgumentOutOfRangeException, o nome do parâmetro vem primeiro. Com um argumento só, o ArgumentNullException monta a mensagem sozinho, em inglês.

ExceçãoQuando lançarExemplo
ArgumentNullExceptionUm parâmetro veio null e não podiaCadastrar(null)
ArgumentOutOfRangeExceptionUm número (ou posição) fora dos limitesDesconto de 150%, idade −1
ArgumentExceptionOutro problema com um parâmetroNome vazio, e-mail sem @
InvalidOperationExceptionOs parâmetros estão certos, mas o objeto não está num estado que permita a operaçãoVender com o caixa fechado; tirar de uma fila vazia
NotImplementedExceptionO método ainda não foi escrito (lembrete temporário)CalcularFrete() => throw new NotImplementedException();
NotSupportedExceptionA operação não existe para este objeto, de propósitoEscrever numa lista só de leitura
Uma exceção suaUm problema do seu domínio, que alguém vai querer capturar separadoSaldoInsuficienteException (aula Exceções Próprias)
Nunca lance estas

Exception e SystemException (gerais demais: quem chama não consegue capturar só o seu problema sem capturar todos), NullReferenceException, IndexOutOfRangeException e StackOverflowException (são do próprio .NET e indicam defeitos; use as Argument…Exception no lugar).


3. Os atalhos de validação .NET 6+

Verificar parâmetros no começo dos métodos é tão comum que o .NET ganhou métodos prontos que fazem o if e o throw numa linha só, e ainda descobrem sozinhos o nome do parâmetro:

void Cadastrar(string? nome, int idade, decimal saldo)
{
    ArgumentException.ThrowIfNullOrWhiteSpace(nome);
    ArgumentOutOfRangeException.ThrowIfNegative(idade);
    ArgumentOutOfRangeException.ThrowIfGreaterThan(idade, 130);
    ArgumentOutOfRangeException.ThrowIfNegative(saldo);
    Console.WriteLine($"{nome}, {idade} anos: ok");
}

void Testar(Action acao)
{
    try { acao(); }
    catch (Exception e) { Console.WriteLine($"{e.GetType().Name}: {e.Message}"); }
}

Testar(() => Cadastrar("Ana", 29, 10m));
Testar(() => Cadastrar("  ", 29, 10m));
Testar(() => Cadastrar(null, 29, 10m));
Testar(() => Cadastrar("Ana", -1, 10m));
Testar(() => Cadastrar("Ana", 200, 10m));
Testar(() => Cadastrar("Ana", 29, -5m));
string? cliente = null;
Testar(() => ArgumentNullException.ThrowIfNull(cliente));
$ dotnet run guarda.cs
Ana, 29 anos: ok
ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'nome')
ArgumentNullException: Value cannot be null. (Parameter 'nome')
ArgumentOutOfRangeException: idade ('-1') must be a non-negative value. (Parameter 'idade')
Actual value was -1.
ArgumentOutOfRangeException: idade ('200') must be less than or equal to '130'. (Parameter 'idade')
Actual value was 200.
ArgumentOutOfRangeException: saldo ('-5') must be a non-negative value. (Parameter 'saldo')
Actual value was -5.
ArgumentNullException: Value cannot be null. (Parameter 'cliente')

O Testar é só um ajudante desta página: ele roda cada chamada dentro de um try e mostra o tipo e a mensagem, para caberem todos os casos num programa só (o () => … é uma lambda, "um pedaço de código guardado para rodar depois").

AtalhoLança quandoDesde
ArgumentNullException.ThrowIfNull(x)x é null.NET 6
ArgumentException.ThrowIfNullOrEmpty(texto)texto null ou "".NET 7
ArgumentException.ThrowIfNullOrWhiteSpace(texto)texto null, vazio ou só com espaços.NET 8
ArgumentOutOfRangeException.ThrowIfNegative(n)n < 0.NET 8
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(n)n <= 0.NET 8
ArgumentOutOfRangeException.ThrowIfZero(n)n == 0.NET 8
ArgumentOutOfRangeException.ThrowIfGreaterThan(n, max)n > max.NET 8
ArgumentOutOfRangeException.ThrowIfLessThan(n, min)n < min.NET 8

A desvantagem: a mensagem é a padrão, em inglês. Quando a mensagem importa (vai aparecer para alguém), use o if com throw new e escreva a sua.


4. Relançar: throw; × throw ex;

Às vezes um catch quer fazer alguma coisa com a exceção (registrar num log, por exemplo) e depois deixá-la continuar subindo, porque não sabe resolvê-la. Para isso, dentro do catch, escreva throw; sozinho, sem nada depois:

Estoque.Carregar("doze");

static class Estoque
{
    public static void Carregar(string texto)
    {
        try
        {
            int quantidade = Ler(texto);
            Console.WriteLine($"Quantidade: {quantidade}");
        }
        catch (FormatException)
        {
            Console.WriteLine($"[log] falhou ao ler \"{texto}\"");
            throw;
        }
    }

    static int Ler(string texto)
    {
        return int.Parse(texto);
    }
}
$ dotnet run relanca.cs
[log] falhou ao ler "doze"
Unhandled exception. System.FormatException: The input string 'doze' was not in a correct format.
   at System.Number.ThrowFormatException[TChar](ReadOnlySpan`1 value)
   at System.Int32.Parse(String s)
   at Estoque.Ler(String texto) in /home/ana/cs/relanca.cs:line 21
   at Estoque.Carregar(String texto) in /home/ana/cs/relanca.cs:line 9
   at Program.<Main>$(String[] args) in /home/ana/cs/relanca.cs:line 1

O catch escreveu o log e o throw; relançou a mesma exceção, com o rastro intacto: dá para ver que ela nasceu no int.Parse, chamado pelo Ler na linha 21, chamado pelo Carregar na linha 9.

Agora a versão que muita gente escreve, dando nome à exceção e relançando com throw e;:

Este código perde informação de propósito.
        catch (FormatException e)
        {
            Console.WriteLine($"[log] falhou ao ler \"{texto}\"");
            throw e;
        }
$ dotnet run relanca2.cs
/home/ana/cs/relanca2.cs(15,13): warning CA2200: Gerar novamente uma exceção detectada altera as informações de pilha (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2200)
[log] falhou ao ler "doze"
Unhandled exception. System.FormatException: The input string 'doze' was not in a correct format.
   at Estoque.Carregar(String texto) in /home/ana/cs/relanca2.cs:line 15
   at Program.<Main>$(String[] args) in /home/ana/cs/relanca2.cs:line 1

Compare os dois rastros. Com throw e;, o rastro recomeça na linha 15, a do próprio throw e;. O Ler e o int.Parse sumiram: quem for consertar o problema vai procurar no lugar errado. O compilador já avisou, na primeira linha: CA2200, "gerar novamente uma exceção detectada altera as informações de pilha". Os avisos CA… vêm dos analisadores de código do .NET, que procuram maus hábitos além dos erros da linguagem.

throw;throw e;
O que relançaA mesma exceçãoA mesma exceção
Stack tracePreservado: mostra onde ela nasceuRecomeça no throw e;
AvisonenhumCA2200
Onde pode ser escritoSó dentro de um catch (senão CS0156)Em qualquer lugar
UseSempre que for relançarNunca para relançar
No editor do guia

O editor desta página roda os mesmos analisadores do dotnet: o aviso CA2200 e o rastro curto do throw e; saem iguais. No rastro do throw; há duas diferenças pequenas: aparecem mais linhas at System.… de dentro do int.Parse, e a linha do Carregar é a 15 (a do throw;), e não a 9 (a da chamada a Ler). O Ler e a linha 21 aparecem nos dois, que é o que importa.


5. Embrulhar: uma exceção dentro da outra

Há uma terceira saída: lançar uma exceção nova, com uma mensagem que faça sentido para quem chamou, guardando a original dentro dela. A original fica na propriedade InnerException ("exceção interna"), e nada se perde:

Estoque.Carregar("doze");

static class Estoque
{
    public static void Carregar(string texto)
    {
        try
        {
            int quantidade = int.Parse(texto);
            Console.WriteLine($"Quantidade: {quantidade}");
        }
        catch (FormatException e)
        {
            throw new InvalidOperationException($"Não deu para carregar o estoque: \"{texto}\".", e);
        }
    }
}
$ dotnet run embrulha.cs
Unhandled exception. System.InvalidOperationException: Não deu para carregar o estoque: "doze".
 ---> System.FormatException: The input string 'doze' was not in a correct format.
   at System.Number.ThrowFormatException[TChar](ReadOnlySpan`1 value)
   at System.Int32.Parse(String s)
   at Estoque.Carregar(String texto) in /home/ana/cs/embrulha.cs:line 9
   --- End of inner exception stack trace ---
   at Estoque.Carregar(String texto) in /home/ana/cs/embrulha.cs:line 14
   at Program.<Main>$(String[] args) in /home/ana/cs/embrulha.cs:line 1
new InvalidOperationException(mensagem, e)
O segundo argumento é a exceção original, que vira a InnerException da nova.
Unhandled exception. System.InvalidOperationException: Não deu…
A exceção de fora, com a sua mensagem.
---> System.FormatException: …
A seta ---> marca a exceção de dentro, com o rastro dela (linha 9, o int.Parse).
--- End of inner exception stack trace ---
"Fim do rastro da exceção interna." Daqui para baixo, é o rastro da exceção de fora (linha 14, o throw new).

Quem captura a exceção de fora pode ler as duas:

try
{
    Estoque.Carregar("doze");
}
catch (InvalidOperationException e)
{
    Console.WriteLine(e.Message);
    Console.WriteLine($"Causa: {e.InnerException?.GetType().Name}: {e.InnerException?.Message}");
}
$ dotnet run embrulha2.cs
Não deu para carregar o estoque: "doze".
Causa: FormatException: The input string 'doze' was not in a correct format.

O ?. está ali porque InnerException pode ser null: nem toda exceção tem uma "de dentro".


6. Expressões throw C# 7

Até aqui, throw foi uma instrução, numa linha própria. Desde o C# 7 ele também pode aparecer no meio de uma expressão, em quatro lugares: depois de ??, como um dos lados do ternário ? :, num braço de uma expressão switch e depois de =>. Todos de uma vez:

var ana = new Cliente("Ana", 29);
Console.WriteLine($"{ana.Nome}, {ana.Idade} anos");
Console.WriteLine(Categoria(2));
try { var x = new Cliente(null, 30); }
catch (ArgumentNullException e) { Console.WriteLine(e.Message); }
try { var y = new Cliente("Bia", -3); }
catch (ArgumentOutOfRangeException e) { Console.WriteLine(e.Message); }
Console.WriteLine(Categoria(9));

string Categoria(int codigo) => codigo switch
{
    1 => "Alimentos",
    2 => "Limpeza",
    _ => throw new ArgumentOutOfRangeException(nameof(codigo), $"Categoria {codigo} não existe.")
};

class Cliente
{
    public string Nome { get; }
    public int Idade { get; }

    public Cliente(string? nome, int idade)
    {
        Nome = nome ?? throw new ArgumentNullException(nameof(nome));
        Idade = idade >= 0 ? idade : throw new ArgumentOutOfRangeException(nameof(idade), "A idade não pode ser negativa.");
    }

    public decimal CalcularFrete() => throw new NotImplementedException();
}
$ dotnet run expr.cs
Ana, 29 anos
Limpeza
Value cannot be null. (Parameter 'nome')
A idade não pode ser negativa. (Parameter 'idade')
Unhandled exception. System.ArgumentOutOfRangeException: Categoria 9 não existe. (Parameter 'codigo')
   at Program.<<Main>$>g__Categoria|0_0(Int32 codigo) in /home/ana/cs/expr.cs:line 14
   at Program.<Main>$(String[] args) in /home/ana/cs/expr.cs:line 8
Nome = nome ?? throw new ArgumentNullException(nameof(nome));
"Use nome; se for null, lance." O ?? (aula Operadores ??, ?. e ??=) só avalia o lado direito quando o esquerdo é null. É a forma mais comum de expressão throw, e de quebra o compilador entende que Nome nunca fica null.
idade >= 0 ? idade : throw new …
No ternário: "se a idade não é negativa, use-a; senão, lance."
_ => throw new …
No switch (aula Expressões switch): o braço _ ("qualquer outro valor") lança. Sem ele, o compilador avisa (CS8509: "a expressão switch não manipula todos os valores possíveis") e um código desconhecido lança uma SwitchExpressionException genérica (Non-exhaustive switch expression failed to match its input), sem a sua mensagem.
CalcularFrete() => throw new NotImplementedException();
Um método ainda por fazer. Compila, e quem chamar descobre na hora que falta escrever.

Fora desses quatro lugares, o throw continua sendo uma instrução:

int x = throw new InvalidOperationException("não");
$ dotnet run tx.cs
/home/ana/cs/tx.cs(1,9): error CS8115: Uma expressão throw não é permitida neste contexto.

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

Missão: o Produto aceita nome null sem reclamar. Troque a linha Nome = nome; por uma expressão ?? throw que lance ArgumentNullException com nameof(nome).

var caderno = new Produto("Caderno");
Console.WriteLine(caderno.Nome);
try
{
    var semNome = new Produto(null);
    Console.WriteLine($"Criado: [{semNome.Nome}]");
}
catch (ArgumentNullException e)
{
    Console.WriteLine(e.Message);
}

class Produto
{
    public string? Nome { get; }

    public Produto(string? nome)
    {
        Nome = nome;
    }
}
var caderno = new Produto("Caderno");
Console.WriteLine(caderno.Nome);
try
{
    var semNome = new Produto(null);
    Console.WriteLine($"Criado: [{semNome.Nome}]");
}
catch (ArgumentNullException e)
{
    Console.WriteLine(e.Message);
}

class Produto
{
    public string? Nome { get; }

    public Produto(string? nome)
    {
        Nome = nome ?? throw new ArgumentNullException(nameof(nome));
    }
}

Formas diferentes, qual usar

SituaçãoForma
Parâmetro inválido, com mensagem suaif (…) throw new ArgumentOutOfRangeException(nameof(x), x, "…");
Parâmetro null, negativo, vazio (mensagem padrão serve)ArgumentNullException.ThrowIfNull(x);, ArgumentOutOfRangeException.ThrowIfNegative(n);…
Guardar um parâmetro que não pode ser nullCampo = valor ?? throw new ArgumentNullException(nameof(valor));
Valor que não cabe em nenhum caso do switch_ => throw new ArgumentOutOfRangeException(…)
Registrar e deixar subircatch (…) { log; throw; }
Trocar por uma exceção com mais sentido para quem chamathrow new OutraException("…", e); (guarda a original em InnerException)
Método ainda não escrito=> throw new NotImplementedException();
Problema próprio do seu programaUma exceção sua: Exceções Próprias

Quando dá errado

CS0029: lançar algo que não é exceção
throw "deu errado";
$ dotnet run erros.cs
/home/ana/cs/erros.cs(1,7): error CS0029: Não é possível converter implicitamente tipo "string" em "System.Exception"

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que significa: só objetos da família Exception podem ser lançados. Um texto não é uma exceção (em JavaScript e em Python dá para lançar quase qualquer coisa; em C#, não).

Como consertar: embrulhe a mensagem numa exceção: throw new InvalidOperationException("deu errado");.

CS0156: throw; fora de um catch
Console.WriteLine("oi");
throw;
$ dotnet run erros2.cs
/home/ana/cs/erros2.cs(2,1): error CS0156: Uma instrução throw sem argumentos não é permitida fora de uma cláusula catch

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que significa: throw; quer dizer "relance a exceção que estou tratando". Fora de um catch, não há exceção nenhuma para relançar.

Como consertar: fora do catch, escreva o que lançar: throw new …;.

Aviso CA2200: o rastro some depois de relançar

O que significa: você escreveu throw e; num catch. O rastro da exceção recomeça ali, e a origem do problema some do relatório.

Como consertar: troque por throw;. Se a ideia era lançar uma exceção nova, passe a original como InnerException: throw new …("…", e);.

A exceção "estranha" que o seu método lançou

O que significa: o relatório diz (Parameter 'percentual'), mas o seu método chama o parâmetro de desconto. Alguém renomeou o parâmetro e esqueceu de trocar o texto escrito à mão.

Como consertar: use nameof(desconto) em vez de "desconto": o compilador acompanha a mudança de nome.


Referência

SintaxeO que faz
throw new Tipo("mensagem");Lança uma exceção nova
new ArgumentException("msg", nameof(p))Mensagem primeiro, parâmetro depois
new ArgumentNullException(nameof(p))Parâmetro primeiro (mensagem opcional depois)
new ArgumentOutOfRangeException(nameof(p), p, "msg")Parâmetro, valor recebido, mensagem
ArgumentNullException.ThrowIfNull(x)Lança se x for null .NET 6
ArgumentOutOfRangeException.ThrowIfNegative(n)Lança se n < 0 .NET 8
throw;Relança a exceção do catch, com o rastro intacto
throw new Tipo("msg", e);Lança uma nova, guardando e em InnerException
x ?? throw new …Expressão throw depois de ?? C# 7
cond ? valor : throw new …Expressão throw no ternário
_ => throw new …Expressão throw num switch
Metodo() => throw new NotImplementedException();Expressão throw depois de =>
CS0029Tentou lançar algo que não é Exception
CS0156throw; fora de um catch
CS8115Expressão throw fora dos quatro lugares permitidos
CA2200 (aviso)throw e; perde o rastro

Exercícios

1. Lançar

Complete para lançar a exceção:

if (preco < 0) new ArgumentOutOfRangeException(nameof(preco));

2. Relançar sem perder o rastro

Complete a última linha do catch:

catch (IOException) { Console.WriteLine("[log] erro de disco"); }

3. O nome do parâmetro

Complete com o operador que transforma o nome do parâmetro em texto:

throw new ArgumentNullException((cliente));

4. Um método Sacar(decimal valor) recebeu valor = -50. Qual exceção ele deve lançar?

5. Qual a diferença entre throw; e throw e; dentro de um catch (Exception e)?

6. Onde uma expressão throw não pode aparecer?

Missão final: o catch relança com throw e;, e o relatório não mostra mais o método Ler, onde o problema nasceu. Conserte o relançamento para o rastro voltar a mostrar Estoque.Ler.

Estoque.Carregar("doze");

static class Estoque
{
    public static void Carregar(string texto)
    {
        try
        {
            int quantidade = Ler(texto);
            Console.WriteLine($"Quantidade: {quantidade}");
        }
        catch (FormatException e)
        {
            Console.WriteLine($"[log] falhou ao ler \"{texto}\": {e.Message}");
            throw e;
        }
    }

    static int Ler(string texto)
    {
        return int.Parse(texto);
    }
}
Estoque.Carregar("doze");

static class Estoque
{
    public static void Carregar(string texto)
    {
        try
        {
            int quantidade = Ler(texto);
            Console.WriteLine($"Quantidade: {quantidade}");
        }
        catch (FormatException e)
        {
            Console.WriteLine($"[log] falhou ao ler \"{texto}\": {e.Message}");
            throw;
        }
    }

    static int Ler(string texto)
    {
        return int.Parse(texto);
    }
}

Resumo

  • throw new Tipo("mensagem"); lança uma exceção: o método para ali e o problema vai para quem chamou, como as exceções do .NET.
  • Escolha uma exceção pronta: ArgumentNullException, ArgumentOutOfRangeException, ArgumentException para parâmetros; InvalidOperationException para o estado do objeto. Nunca lance Exception.
  • Use nameof(parametro) para o nome do parâmetro. Os atalhos ThrowIfNull, ThrowIfNegative… fazem o if e o throw numa linha.
  • Para relançar num catch, throw;. throw e; apaga a origem do rastro (aviso CA2200).
  • Para trocar por uma exceção mais clara, passe a original: throw new …("…", e), e ela fica em InnerException.
  • Expressões throw: ?? throw, ? … : throw, _ => throw e => throw.