C# Exceções

Você já deve ter visto um programa seu parar no meio e despejar na tela umas linhas em inglês começando com Unhandled exception.. Isso é uma exceção, e as linhas que começam com at são o stack trace. Nesta aula você vai entender o que é uma exceção, ler cada pedaço daquela mensagem, achar a linha do seu código no meio dela e conhecer a família das exceções do .NET. Nas próximas aulas você aprende a tratá-las.

Você vai aprender
  • O que é uma exceção e a diferença para um erro de compilação.
  • O que acontece quando uma exceção não é tratada: a mensagem, o código de saída 134 e a saída de erro.
  • Como ler um stack trace: o tipo, a mensagem e cada linha at … in arquivo:line N.
  • Como achar a linha do seu código num stack trace comprido.
  • A família: Exception, SystemException, ArithmeticException, ArgumentException…
  • As exceções que você mais vai encontrar, com as mensagens reais, e as contas erradas que não lançam exceção.

Todos os programas desta aula rodaram na máquina da Ana (usuária ana, computador notebook), com o .NET 10 (C# 14), usando dotnet run arquivo.cs na pasta ~/cs. As mensagens do compilador saem em português, mas as exceções saem em inglês: o .NET no Linux não traduz essas mensagens. A aula traduz cada uma.


A ideia antes do código

A Ana trabalha no caixa da lojinha. O normal é: o cliente traz o produto, ela passa no leitor, cobra e entrega. Mas às vezes algo sai do normal: o código de barras não existe no sistema, a maquininha recusa o cartão, acabou o troco. A Ana não pode fingir que nada aconteceu e seguir em frente. Ela para o atendimento e avisa: "este produto não está cadastrado". Alguém decide o que fazer: ela mesma, ou a gerente.

No C# é igual. Uma exceção (do latim exceptio, "o que fica fora da regra"; em inglês, exception) é um aviso de que aconteceu algo fora do normal enquanto o programa rodava: dividir um inteiro por zero, pegar a posição 10 de um array de 5, transformar "abc" em número. O .NET interrompe o que estava fazendo e lança (throws) o aviso. Se ninguém tratar o aviso, o programa termina ali.

Erro de compilaçãoExceção
Quem encontraO compilador, antes de rodarO .NET, durante a execução
O programa chega a rodar?Não: nenhuma linha é executadaSim: roda até o ponto do problema
Causa típicaCódigo escrito errado (falta ;, tipo errado)Código certo, mas um valor inesperado (zero, null, texto no lugar de número)
Como aparecearquivo.cs(5,13): error CS0029: … (em português)Unhandled exception. System.…Exception: … (em inglês)
AulaLendo Erros do Compiladoresta

O programa "quebrando"

A Ana guardou as notas de cinco provas num array. As posições começam em 0, então as cinco notas estão nas posições 0, 1, 2, 3 e 4. Veja o que acontece quando o programa pede a posição 5:

int[] notas = [7, 9, 6, 10, 8];
Console.WriteLine($"Primeira nota: {notas[0]}");
Console.WriteLine($"Sexta nota: {notas[5]}");
Console.WriteLine("Fim do programa");

O código está escrito corretamente: o compilador não reclama de nada. O problema só aparece quando ele roda:

$ dotnet run notas.cs
Primeira nota: 7
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Program.<Main>$(String[] args) in /home/ana/cs/notas.cs:line 3
$ echo $?
134

Repare em quatro coisas:

  1. A linha 2 rodou: Primeira nota: 7 apareceu. O programa funcionou normalmente até o problema.
  2. A linha 3 lançou a exceção, e o programa parou ali. Nem a própria linha 3 terminou: Sexta nota: não apareceu.
  3. A linha 4 nunca rodou: Fim do programa não aparece.
  4. O código de saída (o número que o programa devolve ao terminar, mostrado com echo $?) foi 134. Um programa que termina bem devolve 0.

É isso que se chama de exceção não tratada (unhandled): ninguém cuidou do aviso, então o .NET mostra o relatório do problema e encerra o programa. A primeira linha diz exatamente isso: Unhandled exception, "exceção não tratada". Na aula try e catch, você aprende a cuidar do aviso e deixar o programa continuar.

Por que 134?

Quando uma exceção não é tratada, o .NET encerra o programa com o sinal SIGABRT ("abortar"), que no Linux tem o número 6. A convenção do Linux para "terminou por causa de um sinal" é 128 + o número do sinal: 128 + 6 = 134. Num script do terminal, um 134 quer dizer "o programa C# quebrou com uma exceção". O número é do Linux (e do macOS); no Windows, o código de saída é outro.

Missão: o programa mostra a primeira nota e termina bem (código 0). Faça-o mostrar a quinta nota (8, na posição 4) e, na linha seguinte, tentar mostrar a sexta nota, que não existe. Rode e leia a exceção e o código de saída.

int[] notas = [7, 9, 6, 10, 8];
Console.WriteLine($"Primeira nota: {notas[0]}");
Console.WriteLine("Fim do programa");
int[] notas = [7, 9, 6, 10, 8];
Console.WriteLine($"Quinta nota: {notas[4]}");
Console.WriteLine($"Sexta nota: {notas[5]}");
Console.WriteLine("Fim do programa");

O relatório vai para a saída de erro

Um programa tem dois "canos" de saída: a saída padrão (stdout), onde vai o Console.WriteLine, e a saída de erro (stderr). Na tela, os dois aparecem misturados. O relatório da exceção vai para a saída de erro, e dá para ver isso mandando-a para um arquivo com 2>:

$ dotnet run notas.cs 2> erros.txt
Primeira nota: 7
$ echo $?
134
$ cat erros.txt
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Program.<Main>$(String[] args) in /home/ana/cs/notas.cs:line 3

Na tela, só ficou o que o programa escreveu de propósito. O relatório foi para o erros.txt. É assim que servidores guardam os problemas num arquivo de registro (log) sem misturar com a saída normal.


Lendo o stack trace, linha por linha

O relatório se chama stack trace (em inglês, "rastro da pilha"). Parece assustador, mas tem sempre as mesmas partes. Este programa tem dois métodos numa classe estática, um chamando o outro:

Console.WriteLine("Começando");
Console.WriteLine(Calculo.Media(30, 0));
Console.WriteLine("Fim");

static class Calculo
{
    public static int Dividir(int a, int b)
    {
        return a / b;
    }

    public static double Media(int soma, int quantidade)
    {
        return Dividir(soma, quantidade);
    }
}

O programa chama Media(30, 0), que chama Dividir(30, 0), que tenta fazer 30 / 0. Divisão de inteiro por zero não existe:

$ dotnet run media.cs
Começando
Unhandled exception. System.DivideByZeroException: Attempted to divide by zero.
   at Calculo.Dividir(Int32 a, Int32 b) in /home/ana/cs/media.cs:line 9
   at Calculo.Media(Int32 soma, Int32 quantidade) in /home/ana/cs/media.cs:line 14
   at Program.<Main>$(String[] args) in /home/ana/cs/media.cs:line 2

A primeira linha, pedaço por pedaço:

Unhandled exception.
"Exceção não tratada." O .NET está avisando que ninguém tratou o problema e que o programa vai terminar.
System.DivideByZeroException
O tipo da exceção, com o nome completo: System é o namespace e DivideByZeroException ("exceção de divisão por zero") é a classe. O nome sozinho já conta muito.
: Attempted to divide by zero.
A mensagem, depois dos dois-pontos: "tentou dividir por zero". Muitas mensagens dizem também o valor que causou o problema.

Cada linha que começa com at ("em") é um degrau da pilha de chamadas:

at Calculo.Dividir(Int32 a, Int32 b)
Onde a exceção nasceu: no método Dividir da classe Calculo. Entre parênteses vêm os parâmetros, com os nomes de tipo do .NET: Int32 é o nome do int (aula Inteiros), String é o do string.
in /home/ana/cs/media.cs:line 9
O arquivo e a linha 9: return a / b;.
at Calculo.Media(…) … line 14
Quem chamou o Dividir: o método Media, na linha 14 (return Dividir(soma, quantidade);).
at Program.<Main>$(String[] args) … line 2
Quem chamou o Media: as instruções do topo do arquivo, na linha 2. <Main>$ é o nome que o compilador dá ao Main escondido das instruções de nível superior, dentro de uma classe Program que ele também cria (aula Program.cs e o Main). É o fundo da pilha, onde tudo começou.

Por que "pilha"?

Pense numa pilha de pratos. Quando o Main chama o Media, o .NET coloca um prato "Media" em cima do prato "Main". Quando o Media chama o Dividir, outro prato vai para o topo. O stack trace é uma foto da pilha no momento da exceção, de cima para baixo:

  ┌──────────────────────────────┐
  │ Dividir   (linha 9)   ← a exceção nasceu aqui (topo)
  ├──────────────────────────────┤
  │ Media     (linha 14)  ← chamou o Dividir
  ├──────────────────────────────┤
  │ <Main>$   (linha 2)   ← chamou o Media (fundo)
  └──────────────────────────────┘

A regra de leitura: a primeira linha at diz onde aconteceu; as de baixo dizem o caminho que levou até lá. Às vezes o erro "de verdade" não está na linha de cima. Aqui, o Dividir faz exatamente o que deveria; quem passou o zero foi a linha 2. O stack trace mostra o caminho todo para você decidir onde consertar.

Uma receita para qualquer stack trace
  1. Leia o tipo (o nome que termina em Exception).
  2. Leia a mensagem depois dos dois-pontos. Não sabe inglês? Copie a mensagem e pesquise: ela é sempre igual, no mundo inteiro.
  3. Desça pelas linhas at até a primeira que cita um arquivo seu (in /home/ana/…) e abra essa linha.
  4. Se a linha estiver certa, desça mais um degrau: quem chamou, e com que valores?

Missão: rode o programa e leia o stack trace. Depois conserte a chamada da linha 2 para calcular a média de 30 pontos em 2 provas. A saída deve ser Começando, 15 e Fim.

Console.WriteLine("Começando");
Console.WriteLine(Calculo.Media(30, 0));
Console.WriteLine("Fim");

static class Calculo
{
    public static int Dividir(int a, int b)
    {
        return a / b;
    }

    public static double Media(int soma, int quantidade)
    {
        return Dividir(soma, quantidade);
    }
}
Console.WriteLine("Começando");
Console.WriteLine(Calculo.Media(30, 2));
Console.WriteLine("Fim");

static class Calculo
{
    public static int Dividir(int a, int b)
    {
        return a / b;
    }

    public static double Media(int soma, int quantidade)
    {
        return Dividir(soma, quantidade);
    }
}

Nomes estranhos: funções locais

Se o método for uma função local, escrita direto no meio das instruções do topo, o nome no stack trace fica esquisito:

Console.WriteLine(Dividir(30, 0));

int Dividir(int a, int b)
{
    return a / b;
}
$ dotnet run local.cs
Unhandled exception. System.DivideByZeroException: Attempted to divide by zero.
   at Program.<<Main>$>g__Dividir|0_0(Int32 a, Int32 b) in /home/ana/cs/local.cs:line 5
   at Program.<Main>$(String[] args) in /home/ana/cs/local.cs:line 1

<<Main>$>g__Dividir|0_0 é o nome que o compilador inventou para a função local Dividir que mora dentro do <Main>$. Não precisa decorar: procure o nome que você conhece (Dividir) no meio dos símbolos, e use o in arquivo:line N, que é sempre claro.

Stack traces compridos: procure o seu arquivo

Quando a exceção nasce dentro da biblioteca do .NET, o rastro mostra também os métodos do próprio .NET. A Ana tentou transformar um texto em número com int.Parse (aula Parse e TryParse):

string idade = "29 anos";
int numero = int.Parse(idade);
Console.WriteLine(numero + 1);
$ dotnet run idade.cs
Unhandled exception. System.FormatException: The input string '29 anos' was not in a correct format.
   at System.Number.ThrowFormatException[TChar](ReadOnlySpan`1 value)
   at System.Int32.Parse(String s)
   at Program.<Main>$(String[] args) in /home/ana/cs/idade.cs:line 2

As duas primeiras linhas at são do .NET (System.Number, System.Int32) e não têm in arquivo:line: você não tem o código-fonte delas, e não é lá que está o problema. A primeira linha sua é a da idade.cs:line 2, a chamada int.Parse(idade). A mensagem conta o motivo: The input string '29 anos' was not in a correct format, "o texto de entrada '29 anos' não estava num formato correto". O texto tem letras, então não vira número.

No editor do guia, o rastro pode ser mais comprido

Rodando o idade.cs no editor desta página, aparecem seis linhas at System.… em vez de duas (System.Number.ParseBinaryInteger…, System.Int32.Parse(ReadOnlySpan`1 s, …)…). É o mesmo caminho: o dotnet do computador junta alguns métodos pequenos do .NET num só, para ganhar velocidade, e o editor não. A linha do seu arquivo e a mensagem são iguais nos dois.


Exceções são objetos

Uma exceção não é só uma mensagem na tela: é um objeto de verdade, criado a partir de uma classe, como os objetos de Classes e Objetos. DivideByZeroException, FormatException e todas as outras são classes. Quando o problema acontece, o .NET cria um objeto dessa classe (com a mensagem e o stack trace guardados dentro) e o lança.

Como toda classe, elas formam uma família por herança. Este programa pergunta a cada classe quem é a "mãe" dela (a propriedade BaseType, da aula Reflexão), até chegar ao topo:

Type[] tipos = [typeof(DivideByZeroException), typeof(FormatException), typeof(IndexOutOfRangeException),
    typeof(ArgumentNullException), typeof(ArgumentOutOfRangeException), typeof(KeyNotFoundException),
    typeof(FileNotFoundException), typeof(NullReferenceException), typeof(InvalidOperationException),
    typeof(OverflowException), typeof(InvalidCastException), typeof(OutOfMemoryException), typeof(StackOverflowException)];
foreach (var t in tipos)
{
    var nomes = new List<string>();
    for (var x = t; x != null; x = x.BaseType) nomes.Add(x.Name);
    Console.WriteLine(string.Join(" → ", nomes));
}
$ dotnet run familia.cs
DivideByZeroException → ArithmeticException → SystemException → Exception → Object
FormatException → SystemException → Exception → Object
IndexOutOfRangeException → SystemException → Exception → Object
ArgumentNullException → ArgumentException → SystemException → Exception → Object
ArgumentOutOfRangeException → ArgumentException → SystemException → Exception → Object
KeyNotFoundException → SystemException → Exception → Object
FileNotFoundException → IOException → SystemException → Exception → Object
NullReferenceException → SystemException → Exception → Object
InvalidOperationException → SystemException → Exception → Object
OverflowException → ArithmeticException → SystemException → Exception → Object
InvalidCastException → SystemException → Exception → Object
OutOfMemoryException → SystemException → Exception → Object
StackOverflowException → SystemException → Exception → Object

Não se preocupe em entender cada linha deste programa agora; o que importa é a resposta. Juntando tudo, a árvore fica assim:

A família das exceções

Object
 └─ Exception ......................... a mãe de todas
     └─ SystemException ............... as que o próprio .NET lança
         ├─ ArithmeticException ....... contas
         │   ├─ DivideByZeroException
         │   └─ OverflowException
         ├─ ArgumentException ......... argumento inválido num método
         │   ├─ ArgumentNullException
         │   └─ ArgumentOutOfRangeException
         ├─ FormatException ........... texto no formato errado
         ├─ IndexOutOfRangeException .. posição fora do array
         ├─ InvalidCastException ...... conversão impossível
         ├─ InvalidOperationException . operação que não cabe agora
         ├─ KeyNotFoundException ...... chave que não existe no dicionário
         ├─ NullReferenceException .... usou algo que é null
         ├─ IOException ............... arquivos e entrada/saída
         │   └─ FileNotFoundException
         ├─ OutOfMemoryException ...... a memória acabou
         └─ StackOverflowException .... a pilha de chamadas estourou
ClasseEm portuguêsO que representa
ExceptionexceçãoA raiz. Só objetos desta família podem ser lançados com throw e capturados com catch.
SystemExceptionexceção do sistemaAs que o próprio .NET lança. Você nunca cria uma SystemException diretamente.
ArithmeticExceptionexceção aritméticaProblemas em contas: divisão de inteiro por zero, estouro em checked.
ArgumentExceptionexceção de argumentoUm método recebeu um valor que não aceita. As filhas dizem o porquê: null (ArgumentNullException) ou fora do intervalo (ArgumentOutOfRangeException).

Essa família importa porque, na próxima aula, um catch de uma classe mãe pega também as filhas: catch (ArithmeticException) pega DivideByZeroException e OverflowException.

Vindo do Java?

O C# não tem exceções "checked". O compilador nunca obriga você a tratar ou declarar uma exceção, e não existe a palavra throws no método. Também não existe o ramo Error: problemas graves, como OutOfMemoryException, também são Exception. Cabe a você, e à documentação de cada método, saber o que pode ser lançado.


As exceções mais comuns

Estas são as que quem está começando mais encontra. Todas foram provocadas de verdade no .NET 10 da Ana; a mensagem é exatamente a que aparece. A lista para consulta está em Referência: Exceções.

ExceçãoCódigo que provocaMensagem real (e tradução)Aula
IndexOutOfRangeExceptionnotas[5] num array de 5; "uva"[9]Index was outside the bounds of the array.
"O índice estava fora dos limites do array."
Arrays
ArgumentOutOfRangeExceptionlista[5] numa List de 2Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index')
"Índice fora do intervalo. Tem de ser não negativo e menor que o tamanho da coleção."
List<T>
ArgumentOutOfRangeException"uva".Substring(9)startIndex cannot be larger than length of string. (Parameter 'startIndex')
"O início não pode ser maior que o tamanho do texto."
Métodos de String
DivideByZeroException30 / 0 com inteiros ou decimalAttempted to divide by zero.
"Tentou dividir por zero."
Aritméticos
FormatExceptionint.Parse("29 anos")The input string '29 anos' was not in a correct format.
"O texto '29 anos' não está num formato correto."
Parse e TryParse
OverflowExceptionint.Parse("99999999999")Value was either too large or too small for an Int32.
"O valor é grande ou pequeno demais para um int."
Parse e TryParse
OverflowExceptionchecked(int.MaxValue + 1)Arithmetic operation resulted in an overflow.
"A operação aritmética resultou num estouro."
Inteiros
ArgumentNullExceptionint.Parse(null)Value cannot be null. (Parameter 's')
"O valor não pode ser null (parâmetro 's')."
null
NullReferenceExceptionapelido.Length com apelido = nullObject reference not set to an instance of an object.
"A referência não aponta para nenhum objeto."
Referências Anuláveis
InvalidCastException(int)coisa com object coisa = "texto"Unable to cast object of type 'System.String' to type 'System.Int32'.
"Não dá para converter um string em int."
Conversão de Tipos
KeyNotFoundExceptionprecos["café"] sem a chave "café"The given key 'café' was not present in the dictionary.
"A chave 'café' não estava no dicionário."
Dictionary
InvalidOperationExceptionvazio.First() numa lista vaziaSequence contains no elements
"A sequência não tem elementos."
LINQ
InvalidOperationExceptionlista.Add(…) dentro de um foreach da mesma listaCollection was modified; enumeration operation may not execute.
"A coleção foi modificada; a enumeração não pode continuar."
foreach
FileNotFoundExceptionFile.ReadAllText("compras.txt") sem o arquivoCould not find file '/home/ana/cs/compras.txt'.
"Não foi possível encontrar o arquivo."
Arquivos

Repare que a FileNotFoundException mostra o caminho completo: "compras.txt" virou /home/ana/cs/compras.txt, porque o programa procura o arquivo na pasta de onde foi rodado. Essa mensagem já resolve metade dos problemas com arquivos: "ah, ele estava procurando na pasta errada".

O aviso que vem antes da exceção

O compilador não consegue prever todas as exceções, mas às vezes ele avisa. Com as referências anuláveis ligadas (o padrão do .NET 10), usar algo que pode ser null dá o aviso CS8602:

string? apelido = null;
Console.WriteLine("Tamanho do apelido:");
Console.WriteLine(apelido.Length);
$ dotnet run nulo.cs
/home/ana/cs/nulo.cs(3,19): warning CS8602: Desreferência de uma referência possivelmente nula.
Tamanho do apelido:
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.<Main>$(String[] args) in /home/ana/cs/nulo.cs:line 3

A primeira linha é do compilador, em português, antes de rodar: "desreferência (uso) de uma referência possivelmente nula", na linha 3, coluna 19. Como é só um aviso, o programa rodou… e quebrou exatamente na linha avisada. Por isso o guia insiste: leia os avisos (aula Avisos do Compilador). Cada um é uma exceção que você pode evitar antes de ela acontecer.


Nem toda conta errada lança exceção

Algumas operações que parecem impossíveis não lançam exceção. Elas devolvem um resultado estranho, e o programa segue como se nada tivesse acontecido. Isso pode ser pior, porque ninguém percebe:

double conta = 1.0 / 0;
Console.WriteLine(conta);
Console.WriteLine(0.0 / 0);
decimal d = 0;
Console.WriteLine(10m / d);
$ dotnet run real.cs
∞
NaN
Unhandled exception. System.DivideByZeroException: Attempted to divide by zero.
   at System.Decimal.DecCalc.VarDecDiv(DecCalc& d1, DecCalc& d2)
   at System.Decimal.op_Division(Decimal d1, Decimal d2)
   at Program.<Main>$(String[] args) in /home/ana/cs/real.cs:line 5
1.0 / 0 → ∞
Com double, dividir por zero dá infinito, sem exceção.
0.0 / 0 → NaN
Not a Number, "não é um número": o resultado de uma conta sem sentido. Também sem exceção (aula float, double e decimal).
10m / d → DivideByZeroException
Com decimal (e com inteiros), dividir por zero lança a exceção. Mais um motivo para usar decimal com dinheiro: o erro aparece, em vez de um "infinito" silencioso numa nota fiscal.

Com inteiros, há outro caso silencioso: o estouro. Passar do maior int faz o número "dar a volta", a menos que a conta esteja dentro de checked:

int grande = int.MaxValue;
Console.WriteLine(grande + 1);
Console.WriteLine(checked(grande + 1));
$ dotnet run estouro.cs
-2147483648
Unhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow.
   at Program.<Main>$(String[] args) in /home/ana/cs/estouro.cs:line 3

A linha 2 mostrou um número negativo sem reclamar; a linha 3, com checked ("verificado"), lançou OverflowException. Os detalhes estão em Inteiros.


A exceção que não dá para tratar: estouro da pilha

Um método que chama a si mesmo para sempre (uma recursão sem fim) empilha pratos até a pilha acabar:

Contar(1);

void Contar(int n)
{
    Contar(n + 1);
}
$ dotnet run pilha.cs
Stack overflow.
Repeated 261792 times:
--------------------------------
   at Program.<<Main>$>g__Contar|0_0(Int32)
--------------------------------
   at Program.<Main>$(System.String[])
$ echo $?
134

"Estouro da pilha. Repetido 261792 vezes": o .NET resume as chamadas iguais em vez de mostrar 261 mil linhas (o número muda de uma execução para outra). Esse é o único caso da aula que nenhum catch consegue segurar: o .NET encerra o programa na hora. A solução é sempre consertar o código, para a recursão ter um ponto de parada.


Prevenir ou tratar?

Muitas exceções podem ser evitadas com um if antes da linha perigosa. Quando o problema é previsível, prevenir é mais claro (e mais rápido) do que deixar a exceção acontecer:

int[] notas = [7, 9, 6, 10, 8];
int posicao = 5;
if (posicao < notas.Length)
{
    Console.WriteLine($"Nota {posicao + 1}: {notas[posicao]}");
}
else
{
    Console.WriteLine($"Não existe a nota {posicao + 1}: só há {notas.Length}.");
}
$ dotnet run prevenir.cs
Não existe a nota 6: só há 5.
$ echo $?
0
ProblemaPrevenir comTratar com
Posição fora do arrayif (i < notas.Length)catch (IndexOutOfRangeException)
Texto que não é númeroint.TryParse(texto, out int n)catch (FormatException)
Divisão por zeroif (quantidade != 0)catch (DivideByZeroException)
Variável null?., ??, if (x is not null) e os avisos CS86xx(não trate: conserte o código)
Chave fora do dicionárioprecos.TryGetValue("café", out var p)catch (KeyNotFoundException)
Arquivo que pode sumir, rede que pode cair(não dá para prevenir tudo: outro programa pode apagar o arquivo um instante depois do if)catch (IOException)

A regra prática: previna o que é erro de lógica ou de entrada do usuário; trate (com try e catch) o que depende do mundo lá fora: arquivos, rede, outros programas.


Quando dá errado

O programa fecha e mostra um monte de linhas em inglês

O que aconteceu: uma exceção não tratada. O código de saída é 134.

Como resolver: siga a receita: o tipo, a mensagem e a primeira linha at com in /home/ana/…:line N. Abra o arquivo nessa linha e pergunte "que valor aqui pode estar diferente do que eu esperava?".

O stack trace não mostra nenhum arquivo meu

O que aconteceu: quase sempre, você está olhando só o topo de um rastro comprido. As linhas do .NET (at System.…, sem in arquivo) vêm primeiro; as suas, mais embaixo.

Como resolver: role até o fim do relatório. A linha at Program.<Main>$(…) in …, quando existe, é a última.

A linha do stack trace parece certa

O que aconteceu: a linha que lançou está certa; quem errou foi quem passou o valor. No media.cs, a linha 9 (a / b) não tem defeito; o zero veio da linha 2.

Como resolver: desça um degrau no stack trace de cada vez, até achar de onde veio o valor errado.

O programa não quebra, mas o resultado é ∞, NaN ou um número negativo absurdo

O que aconteceu: uma conta com double dividiu por zero, ou um inteiro estourou. Nenhuma exceção foi lançada.

Como resolver: confira o divisor antes da conta, use decimal para dinheiro e checked onde um estouro seria grave.


Referência

Parte do relatórioO que é
Unhandled exception."Exceção não tratada": ninguém capturou, o programa vai terminar
System.FormatExceptionO tipo (namespace + classe)
: The input string '…' was not …A mensagem (sempre em inglês no Linux)
at Classe.Metodo(Int32 a) in /home/ana/cs/x.cs:line 9Um degrau da pilha: método, parâmetros, arquivo e linha
at System.… (sem in)Código do .NET: pule até achar um arquivo seu
Program.<Main>$O Main escondido das instruções de nível superior
<<Main>$>g__Nome|0_0Uma função local chamada Nome
Código de saída 134128 + 6 (SIGABRT): o programa terminou por uma exceção não tratada
2> erros.txtO relatório vai para a saída de erro; assim ele fica num arquivo
Stack overflow.Recursão sem fim; não pode ser capturada

Exercícios

1. O código de saída

Uma exceção não tratada num programa C# no Linux faz o echo $? mostrar:

2. O nome da exceção

Complete o nome da exceção que int.Parse("dez") lança:

System.: The input string 'dez' was not in a correct format.

3. A mãe de todas

Complete: todas as exceções do .NET descendem da classe

4. Num stack trace, qual linha at mostra onde a exceção nasceu?

5. Qual destas linhas não lança exceção?

6. DivideByZeroException e OverflowException são filhas de qual classe?

Missão final: o programa quebra com IndexOutOfRangeException (código 134). Sem tirar a linha que mostra a nota, previna a exceção com um if que compare posicao com notas.Length. Quando a nota não existir, mostre Não existe a nota 6. O programa deve terminar com código 0.

int[] notas = [7, 9, 6, 10, 8];
int posicao = 5;
Console.WriteLine($"Nota {posicao + 1}: {notas[posicao]}");
int[] notas = [7, 9, 6, 10, 8];
int posicao = 5;
if (posicao < notas.Length)
{
    Console.WriteLine($"Nota {posicao + 1}: {notas[posicao]}");
}
else
{
    Console.WriteLine($"Não existe a nota {posicao + 1}: só há {notas.Length}.");
}

Resumo

  • Uma exceção é um aviso de que algo saiu do normal durante a execução. O código compila; o problema está num valor.
  • Sem tratamento, o programa para na linha do problema, mostra Unhandled exception. com o tipo, a mensagem (em inglês) e o stack trace, na saída de erro, e termina com o código 134.
  • No stack trace, a primeira linha at é onde a exceção nasceu; as de baixo, o caminho até lá. Procure o in /home/ana/…:line N do seu arquivo.
  • Exceções são objetos de classes que herdam de Exception. As do .NET passam por SystemException. O C# não tem exceções "checked".
  • As mais comuns: IndexOutOfRangeException, ArgumentOutOfRangeException, FormatException, NullReferenceException, DivideByZeroException, KeyNotFoundException, InvalidOperationException, FileNotFoundException.
  • double dividido por zero dá ∞ ou NaN, e inteiros estouram em silêncio fora de checked: sem exceção.
  • Previna com if e TryParse o que é previsível; trate com try/catch (próxima aula) o que depende do mundo lá fora.