C# finally

Na aula try e catch, você aprendeu a tratar uma exceção. Mas há coisas que precisam acontecer dando certo ou dando errado: fechar um arquivo, devolver uma conexão, mostrar "fim do atendimento". Para isso existe um terceiro bloco, o finally ("finalmente"). Nesta aula você vai ver quando ele roda, em que ordem, como ele salva os dados de um arquivo e os poucos casos em que ele não roda.

Você vai aprender
  • Escrever try/catch/finally e o caminho do programa em cada caso.
  • Usar try/finally sem catch, deixando a exceção subir.
  • O que acontece com o finally quando a exceção não é tratada.
  • O finally junto com return, break e continue.
  • Fechar um arquivo no finally, e o que se perde quando ele não é fechado.
  • Os erros CS0157, CS1524 e CS0103, e quando o finally não roda.

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

No fim do expediente, a Ana fecha o caixa da lojinha. Não importa como foi o dia: vendeu muito, vendeu pouco, a maquininha deu problema, faltou troco. O caixa sempre é fechado, contado e trancado antes de ela ir embora. Se ela esquecer num dia ruim, justamente no dia em que tudo deu errado, o prejuízo é maior.

O finally é esse "fechar o caixa": um bloco que roda sempre no fim do try, tenha acontecido exceção ou não, tenha ela sido tratada ou não.


1. try, catch e finally

O programa abaixo registra duas vendas. A primeira dá certo; a segunda tem um valor inválido:

string[] valores = ["10", "abc"];
foreach (string v in valores)
{
    Console.WriteLine($"--- {v} ---");
    try
    {
        Console.WriteLine("Abrindo o caixa");
        int n = int.Parse(v);
        Console.WriteLine($"Venda de R$ {n}");
    }
    catch (FormatException)
    {
        Console.WriteLine("Valor inválido");
    }
    finally
    {
        Console.WriteLine("Fechando o caixa");
    }
}
$ dotnet run caixa.cs
--- 10 ---
Abrindo o caixa
Venda de R$ 10
Fechando o caixa
--- abc ---
Abrindo o caixa
Valor inválido
Fechando o caixa
try { … }
O código vigiado, como na aula anterior.
catch (FormatException) { … }
Roda só se o try lançar FormatException.
finally
"Finalmente." Vem depois de todos os catch, e só pode haver um.
{ Console.WriteLine("Fechando o caixa"); }
Roda sempre: depois do try que deu certo, ou depois do catch que tratou o problema.

Os dois caminhos, lado a lado:

BlocoCom "10" (tudo certo)Com "abc" (exceção)
tryroda inteiroroda até o int.Parse; o resto é pulado
catch (FormatException)puladoroda
finallyrodaroda
depois do bloco todorodaroda

Se é para rodar sempre, por que não escrever a linha simplesmente depois do try/catch? Porque "depois" nem sempre é alcançado: a exceção pode não ser do tipo do catch, o catch pode lançar outra, o método pode ter um return no meio. O finally roda em todos esses casos, como as próximas seções mostram.

Missão: o programa "esquece" de fechar o caixa. Acrescente um finally, depois do catch, que mostre Fechando o caixa. A mensagem deve aparecer nas duas vendas.

string[] valores = ["10", "abc"];
foreach (string v in valores)
{
    Console.WriteLine($"--- {v} ---");
    try
    {
        Console.WriteLine("Abrindo o caixa");
        int n = int.Parse(v);
        Console.WriteLine($"Venda de R$ {n}");
    }
    catch (FormatException)
    {
        Console.WriteLine("Valor inválido");
    }
}
string[] valores = ["10", "abc"];
foreach (string v in valores)
{
    Console.WriteLine($"--- {v} ---");
    try
    {
        Console.WriteLine("Abrindo o caixa");
        int n = int.Parse(v);
        Console.WriteLine($"Venda de R$ {n}");
    }
    catch (FormatException)
    {
        Console.WriteLine("Valor inválido");
    }
    finally
    {
        Console.WriteLine("Fechando o caixa");
    }
}

2. try/finally sem catch

O catch é opcional. Um try pode ter só o finally. Isso faz sentido quando o método não sabe tratar o problema, mas precisa arrumar a casa antes de a exceção subir para quem o chamou:

try
{
    Vender("abc");
}
catch (FormatException)
{
    Console.WriteLine("Main: valor inválido");
}

void Vender(string valor)
{
    try
    {
        Console.WriteLine("Abrindo o caixa");
        int n = int.Parse(valor);
        Console.WriteLine($"Venda de R$ {n}");
    }
    finally
    {
        Console.WriteLine("Fechando o caixa");
    }
}
$ dotnet run sobe.cs
Abrindo o caixa
Fechando o caixa
Main: valor inválido

Siga a ordem: o int.Parse lançou dentro de Vender. Ali não há catch, então a exceção vai subir, mas antes de sair do método o finally roda (Fechando o caixa). Só então a exceção chega às instruções do topo, onde o catch a trata (Main: valor inválido).

As combinações permitidas são três: try/catch, try/finally e try/catch/finally. Um try sozinho é erro (CS1524, em Quando dá errado).


3. E se ninguém tratar a exceção?

Sem nenhum catch no caminho, a exceção encerra o programa. O finally roda mesmo assim, mas repare quando:

try
{
    Console.WriteLine("Abrindo o caixa");
    int n = int.Parse("abc");
}
finally
{
    Console.WriteLine("Fechando o caixa");
}
Console.WriteLine("Fim");
$ dotnet run naotratada.cs
Abrindo o caixa
Unhandled exception. System.FormatException: The input string 'abc' 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/naotratada.cs:line 4
Fechando o caixa
$ echo $?
134

O relatório da exceção apareceu antes do Fechando o caixa. O .NET trabalha em duas etapas: primeiro ele procura um catch em toda a pilha; como não achou nenhum, já escreve o relatório. Depois ele desce a pilha rodando os finally e, por fim, encerra o programa com 134. O Fim, que está fora do try, nunca roda.

No editor do guia, o rastro é mais comprido

Rodando este programa no editor desta página, a ordem é a mesma: o relatório, depois o Fechando o caixa e o código de saída 134. Só o rastro muda um pouco: aparecem mais linhas at System.… de dentro do int.Parse (seis em vez de duas). A linha do seu arquivo (naotratada.cs:line 4) e a mensagem são iguais nos dois.


4. finally com return, break e continue

O finally também roda quando o try termina "pulando para fora" sem exceção nenhuma. O caso mais comum é um return:

Console.WriteLine(Dobro(21));

int Dobro(int n)
{
    try
    {
        Console.WriteLine("No try, antes do return");
        return n * 2;
    }
    finally
    {
        Console.WriteLine("No finally");
    }
}
$ dotnet run retorno.cs
No try, antes do return
No finally
42

O return n * 2; calcula o valor (42) e se prepara para sair do método. Antes de sair, roda o finally (No finally). Só depois o 42 chega ao Console.WriteLine da linha 1, que o mostra.

Num laço, break e continue (aula break e continue) também passam pelo finally:

for (int i = 1; i <= 3; i++)
{
    try
    {
        if (i == 2) continue;
        if (i == 3) break;
        Console.WriteLine($"Volta {i}");
    }
    finally
    {
        Console.WriteLine($"finally da volta {i}");
    }
}
$ dotnet run laco.cs
Volta 1
finally da volta 1
finally da volta 2
finally da volta 3
Volta 1
O try rodou inteiro; depois, o finally.
Volta 2
O continue pulou para a próxima volta, mas passou pelo finally antes.
Volta 3
O break saiu do laço, também passando pelo finally.

5. O uso de verdade: fechar o que foi aberto

Mostrar mensagens é bom para entender, mas o trabalho real do finally é liberar recursos: fechar arquivos, conexões com bancos de dados, conexões de rede. A Ana grava a lista de compras num arquivo com um StreamWriter ("escritor de fluxo", aula StreamReader e StreamWriter). No meio, uma quantidade inválida lança uma exceção:

Este código perde dados de propósito.
var escritor = new StreamWriter("compras.txt");
try
{
    escritor.WriteLine("arroz");
    escritor.WriteLine("feijão");
    int quantidade = int.Parse("duas");
    escritor.WriteLine($"café x{quantidade}");
}
catch (FormatException)
{
    Console.WriteLine("Quantidade inválida");
}
Console.WriteLine("Fim do programa");
$ dotnet run lista.cs
Quantidade inválida
Fim do programa
$ ls -l compras.txt
-rw-rw-r-- 1 ana ana 0 out 17 13:02 compras.txt
$ cat compras.txt

O arquivo foi criado, mas tem 0 bytes: nem o arroz nem o feijão, que foram escritos antes da exceção, chegaram ao disco. O motivo: o StreamWriter guarda o texto numa área de memória (o buffer) e só grava no disco de vez em quando, ou quando é fechado. Ninguém o fechou, e o texto se perdeu quando o programa terminou.

Com o Close() ("fechar") no finally, o arquivo é fechado com ou sem exceção:

var escritor = new StreamWriter("compras.txt");
try
{
    escritor.WriteLine("arroz");
    escritor.WriteLine("feijão");
    int quantidade = int.Parse("duas");
    escritor.WriteLine($"café x{quantidade}");
}
catch (FormatException)
{
    Console.WriteLine("Quantidade inválida");
}
finally
{
    escritor.Close();
    Console.WriteLine("Arquivo fechado");
}
Console.WriteLine("Fim do programa");
$ dotnet run lista2.cs
Quantidade inválida
Arquivo fechado
Fim do programa
$ cat compras.txt
arroz
feijão
var escritor = new StreamWriter("compras.txt");
Abre (e cria) o arquivo. Fica fora do try: assim a variável existe no finally. Se a abertura falhar, não há nada para fechar, e o finally nem é alcançado.
escritor.WriteLine("arroz");
Escreve no buffer, ainda não no disco.
escritor.Close();
Grava o que estava no buffer e fecha o arquivo.

O café não entrou, porque a linha dele nunca rodou, mas o que já tinha sido escrito foi salvo. É isso que o finally garante.

Missão: rode e veja que o arquivo sai vazio (depois de No arquivo: não aparece nada). Acrescente um finally que feche o escritor com escritor.Close(); e rode de novo: o arroz e o feijão devem aparecer.

var escritor = new StreamWriter("compras.txt");
try
{
    escritor.WriteLine("arroz");
    escritor.WriteLine("feijão");
    int quantidade = int.Parse("duas");
    escritor.WriteLine($"café x{quantidade}");
}
catch (FormatException)
{
    Console.WriteLine("Quantidade inválida");
}
Console.WriteLine("No arquivo:");
Console.WriteLine(File.ReadAllText("compras.txt"));
var escritor = new StreamWriter("compras.txt");
try
{
    escritor.WriteLine("arroz");
    escritor.WriteLine("feijão");
    int quantidade = int.Parse("duas");
    escritor.WriteLine($"café x{quantidade}");
}
catch (FormatException)
{
    Console.WriteLine("Quantidade inválida");
}
finally
{
    escritor.Close();
}
Console.WriteLine("No arquivo:");
Console.WriteLine(File.ReadAllText("compras.txt"));

Quando a abertura também pode falhar

Às vezes a própria abertura precisa estar dentro do try, porque é ela que pode lançar (uma pasta que não existe, um arquivo sem permissão). Aí a variável é declarada antes, valendo null, e o finally só fecha se ela chegou a ser criada:

StreamWriter? escritor = null;
try
{
    escritor = new StreamWriter("/pasta-que-nao-existe/compras.txt");
    escritor.WriteLine("arroz");
}
catch (DirectoryNotFoundException e)
{
    Console.WriteLine($"Não deu para criar: {e.Message}");
}
finally
{
    escritor?.Close();
    Console.WriteLine("finally rodou");
}
$ dotnet run nulo.cs
Não deu para criar: Could not find a part of the path '/pasta-que-nao-existe/compras.txt'.
finally rodou
StreamWriter? escritor = null;
O ? diz que a variável pode ficar null (aula Referências Anuláveis). Ela nasce fora do try para existir no finally.
DirectoryNotFoundException
"Pasta não encontrada": Could not find a part of the path, "não foi possível encontrar uma parte do caminho".
escritor?.Close();
O ?. só chama Close() se escritor não for null (aula Operadores ??, ?. e ??=). Aqui ele é null, porque a abertura falhou, e nada acontece.
Existe um jeito mais curto: using

Abrir, usar e fechar no finally é tão comum que o C# tem um atalho para isso: using var escritor = new StreamWriter("compras.txt");. O compilador gera o try/finally sozinho. É a forma que o guia usa com arquivos, e ela tem aula própria: using e IDisposable. Vale entender o finally primeiro, porque é exatamente isso que o using faz por baixo.


Formas diferentes, qual usar

SituaçãoForma
Tratar um problematry/catch
Arrumar a casa, mas deixar a exceção subirtry/finally
Tratar e arrumar a casatry/catch/finally
Fechar arquivo, conexão ou qualquer coisa com Dispose()using (é um try/finally pronto): using e IDisposable
Algo que só deve rodar quando deu certoNo fim do try, não no finally
Algo que só deve rodar quando deu erradoNo catch, não no finally

Quando dá errado

CS1524: "Catch ou finally esperado"
try
{
    Console.WriteLine("oi");
}
Console.WriteLine("fim");
$ dotnet run sotry.cs
/home/ana/cs/sotry.cs(4,1): error CS1524: Catch ou finally esperado

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

O que significa: um try sozinho não serve para nada; depois das chaves dele, o compilador esperava um catch ou um finally.

Como consertar: acrescente o catch ou o finally que faltou, ou tire o try.

CS0157: return dentro do finally
Console.WriteLine(Teste());
int Teste()
{
    try
    {
        return 1;
    }
    finally
    {
        return 2;
    }
}
$ dotnet run retfin.cs
/home/ana/cs/retfin.cs(10,9): error CS0157: O controle não pode sair do corpo de uma cláusula finally

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

O que significa: "o controle não pode sair do corpo de um finally". O finally é para arrumar a casa, não para decidir o resultado. return, break e continue que saiam dele são proibidos (em Java isso é permitido, e é fonte de defeitos difíceis de achar).

Como consertar: deixe o return no try (ou depois do bloco) e use o finally só para liberar recursos.

CS0103: a variável do try não existe no finally
try
{
    var escritor = new StreamWriter("compras.txt");
    escritor.WriteLine("arroz");
}
finally
{
    escritor.Close();
}
$ dotnet run escopo.cs
/home/ana/cs/escopo.cs(8,5): error CS0103: O nome "escritor" não existe no contexto atual

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

O que significa: o try e o finally são blocos separados; o que nasce num não existe no outro (aula Escopo).

Como consertar: declare a variável antes do try, como em lista2.cs ou em nulo.cs. Ou use using.

O finally não rodou

Há poucos casos, todos em que o programa é encerrado "à força":

try
{
    Console.WriteLine("Saindo com Environment.Exit");
    Environment.Exit(3);
}
finally
{
    Console.WriteLine("finally");
}
$ dotnet run sair.cs
Saindo com Environment.Exit
$ echo $?
3

O que significa: Environment.Exit(3) encerra o programa na hora, com o código de saída 3, sem passar pelos finally. O mesmo acontece num estouro da pilha (Stack overflow., aula Exceções), quando a memória acaba ou quando o processo é morto de fora (kill -9, falta de luz).

Como consertar: evite Environment.Exit no meio do programa; prefira terminar normalmente (um return nas instruções do topo). E nunca confie no finally para algo que precisa sobreviver a uma queda de energia: para isso, grave os dados assim que puder.


Referência

Sintaxe / casoO que acontece
try { … } finally { … }O finally roda sempre; a exceção, se houver, continua subindo
try { … } catch (T) { … } finally { … }O finally roda depois do try ou do catch
return no tryO valor é calculado, o finally roda, e só então o método devolve
break / continue no tryO finally roda antes de sair da volta
Exceção não tratadaO relatório sai primeiro, depois roda o finally, depois o programa termina com 134
Environment.Exit(n), Stack overflow, kill -9O finally não roda
CS1524try sem catch nem finally
CS0157return, break ou continue saindo do finally
CS0103Variável declarada no try usada no finally

Exercícios

1. O bloco que sempre roda

Complete:

try { … } catch (FormatException) { … } { escritor.Close(); }

2. Fechar se existir

Complete com o operador que só chama Close() se escritor não for null:

escritorClose();

3. O código do erro

Que erro (CSxxxx) dá um return dentro do finally?

4. O que este trecho mostra?

try
{
    Console.WriteLine("A");
    int x = int.Parse("x");
    Console.WriteLine("B");
}
catch (FormatException)
{
    Console.WriteLine("C");
}
finally
{
    Console.WriteLine("D");
}
Console.WriteLine("E");

5. Um método tem try { return 5; } finally { Console.WriteLine("F"); }. O que acontece ao chamá-lo?

6. Em qual caso o finally não roda?

Missão final: o método Vender não sabe tratar o valor inválido (quem trata é o código do topo), mas precisa fechar o caixa de qualquer jeito. Acrescente ao try de Vender um finally (sem catch!) que mostre Fechando o caixa.

try
{
    Vender("abc");
}
catch (FormatException)
{
    Console.WriteLine("Main: valor inválido");
}

void Vender(string valor)
{
    Console.WriteLine("Abrindo o caixa");
    int n = int.Parse(valor);
    Console.WriteLine($"Venda de R$ {n}");
}
try
{
    Vender("abc");
}
catch (FormatException)
{
    Console.WriteLine("Main: valor inválido");
}

void Vender(string valor)
{
    try
    {
        Console.WriteLine("Abrindo o caixa");
        int n = int.Parse(valor);
        Console.WriteLine($"Venda de R$ {n}");
    }
    finally
    {
        Console.WriteLine("Fechando o caixa");
    }
}

Resumo

  • finally vem depois dos catch e roda sempre: com sucesso, com exceção tratada e com exceção não tratada (nesse caso, depois do relatório).
  • try/finally sem catch arruma a casa e deixa a exceção subir para quem sabe tratá-la.
  • return, break e continue no try passam pelo finally antes de sair. Sair de dentro do finally é proibido (CS0157).
  • O uso de verdade é liberar recursos: um StreamWriter que não é fechado pode perder tudo o que foi escrito.
  • A variável do recurso nasce antes do try (senão, CS0103); se a abertura pode falhar, comece com null e feche com ?.Close().
  • O finally não roda com Environment.Exit, estouro da pilha ou processo morto.
  • Na prática, para arquivos, use using, que gera o try/finally para você.