CodeGym /Cursos /C# SELF /Tratamento de exceções em código assíncrono

Tratamento de exceções em código assíncrono

C# SELF
Nível 59 , Lição 4
Disponível

1. Métodos assíncronos e exceções

Estamos acostumados que se algo ruim acontece no código (por exemplo, divisão por zero ou tentativa de acessar um arquivo inexistente), uma exceção é lançada e podemos capturá‑la com try-catch. Tudo é simples enquanto o código executa sequencialmente e em um único thread. Mas quando surge a assíncronicidade, o mundo fica parecido com o espaço sideral: a exceção pode "aparecer" bem longe do local onde a esperávamos, ou até ficar despercebida.

A razão é que um método assíncrono frequentemente retorna uma tarefa (Task), cuja execução continua DEPOIS de sair do método. A exceção pode acontecer depois que a thread principal "liberou" a execução e segue sua vida. Por isso a construção habitual try-catch em volta da chamada de um método assíncrono nem sempre funciona como no código síncrono.

Vamos entender com um exemplo simples. Suponha que no nosso mini‑app exista este método assíncrono:

// Fragmento do nosso aplicativo: cálculo assíncrono "envio de relatório"
public async Task SendReportAsync()
{
    // Aqui poderiam ser chamadas de rede ou acesso a arquivos
    await Task.Delay(100);
    throw new InvalidOperationException("Erro ao enviar o relatório!");
}

E aqui como podemos chamá‑lo:

SendReportAsync();
Console.WriteLine("Continuamos trabalhando...");

Visualização

flowchart TD
    Start["Main thread"]
    Call[/"Chamada SendReportAsync()"/]
    Continue["Trabalho continua..."]
    Exception["Exceção ocorre na Task"]
    Unhandled["Erro não tratado!"]
    Start --> Call --> Continue
    Call -.- Exception --> Unhandled

Conclusão: se o método assíncrono retorna Task e você não espera a conclusão da tarefa (await ou .Wait()), a exceção ficará "despercebida". No melhor cenário, o runtime vai escrever no log algo como "Unhandled exception in task". No pior — você perde o erro e passa um bom tempo procurando a origem de bugs "místicos".

2. Como capturar exceções no código assíncrono?

Use await + try-catch

Considere a forma correta:

try
{
    await SendReportAsync(); // Esperamos pela conclusão do Task
    Console.WriteLine("Relatório enviado com sucesso!");
}
catch (Exception ex)
{
    Console.WriteLine($"Ops! Algo deu errado: {ex.Message}");
}

Como isso funciona? Quando você coloca await antes da chamada de um método assíncrono, o C# divide seu método em duas partes: antes e depois do await. Se a parte assíncrona lançar uma exceção, ela "surgirá" exatamente no ponto onde o await foi usado, e aí pode ser capturada pelo clássico try-catch.

Exemplo para a aplicação

Vamos adicionar tratamento de erro no envio do relatório no nosso demo:

public async Task StartReportProcessAsync()
{
    try
    {
        await SendReportAsync();
        Console.WriteLine("Relatório enviado com sucesso!");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Erro ao enviar o relatório: {ex.Message}");
    }
}

E chamar:

await StartReportProcessAsync();

.Wait(), .Result — não são a melhor, mas uma tática que funciona em console

Às vezes, especialmente em aplicações de console, você não pode usar await no topo (versões antigas do C#, método Main). Aí é necessário esperar sincronicamente a conclusão da task usando .Wait() ou .Result.

try
{
    SendReportAsync().Wait();
}
catch (AggregateException aggEx)
{
    foreach (var ex in aggEx.InnerExceptions)
        Console.WriteLine($"Erro: {ex.Message}");
}

Por que assim? Chamadas a .Wait() e .Result sempre embrulham a exceção original em um AggregateException. Esse é um container que pode conter uma ou várias exceções internas. Pode haver uma (ou várias!) exceções internas, por isso é preciso desempacotá‑las com um loop. Leia mais sobre AggregateException na documentação oficial.

Importante!

Em versões modernas do .NET (a partir do C# 7.1) você pode declarar o Main como assíncrono e usar await diretamente na entrada:

static async Task Main(string[] args)
{
    await StartReportProcessAsync();
}

3. Exceções em tarefas "fire-and-forget"

O que acontece se você iniciar um método assíncrono sem esperar sua conclusão e sem guardar a referência da task?

SendReportAsync(); // "Esquecemos" da task

Nesse caso surge um problema: a exceção que ocorrer na task não será tratada por ninguém. Às vezes (dependendo do ambiente e das configurações) a aplicação pode até terminar com falha. Outras vezes apenas um aviso é logado. Isso não é um bug do C#, é consequência da lógica de funcionamento das tasks.

Como fazer corretamente?

  • No ideal: nunca use "fire-and-forget" se você não tem certeza de que a tarefa não pode falhar criticamente.
  • Se o método assíncrono realmente precisa rodar em "fire-and-forget", trate erros explicitamente dentro do método.
public async Task SendReportSafeAsync()
{
    try
    {
        await Task.Delay(100);
        throw new InvalidOperationException("Erro ao enviar!");
    }
    catch (Exception ex)
    {
        // Logamos ou tratamos o erro
        Console.WriteLine($"[Log] Exceção: {ex.Message}");
    }
}

// Chamada
SendReportSafeAsync();

Recomendação geral: Se a tarefa não é observada por ninguém e você não usa await, envolva o corpo do método assíncrono em try-catch. Assim você não perde o erro e ao menos pode logá‑lo.

4. Exceções e tarefas paralelas: Task.WhenAll e afins

Frequentemente em apps reais é necessário iniciar várias tarefas assíncronas independentes e esperar que todas terminem. Por exemplo, quando você envia relatórios para vários destinatários em paralelo:

var tasks = new List<Task>
{
    SendReportAsync(),
    SendReportAsync(),
    SendReportAsync()
};

await Task.WhenAll(tasks);

O que acontece se uma (ou várias) tasks lançarem exceção?

Como capturar esses erros?

Ao usar await com Task.WhenAll(tasks) — se pelo menos uma task terminar com erro, o await relançará a exceção da primeira task que terminou com erro (ela não será embrulhada em AggregateException).
Mas tem um detalhe: se houver múltiplas falhas entre as tasks, então será lançada uma AggregateException com o conjunto de exceções internas.

try
{
    await Task.WhenAll(tasks);
}
catch (Exception ex)
{
    // Se for AggregateException — vamos desembrulhar
    if (ex is AggregateException agg)
    {
        foreach (var inner in agg.InnerExceptions)
            Console.WriteLine($"Erro na task: {inner.Message}");
    }
    else
    {
        Console.WriteLine($"Erro: {ex.Message}");
    }
}

Para await com uma task única, a exceção geralmente não é embrulhada em AggregateException. Mas com WhenAll isso pode ocorrer!

5. Delegados assíncronos e tratamento de erros

Em aplicações com interface (WPF, WinForms, ASP.NET) handlers de evento frequentemente são implementados como lambdas assíncronas. Se uma exceção nesse handler "vazar" para fora, o comportamento depende do framework UI: a aplicação pode terminar com falha ou simplesmente engolir o erro.

Recomendação

Sempre use try-catch dentro de delegados assíncronos:

button.Click += async (sender, args) =>
{
    try
    {
        await SendReportAsync();
    }
    catch (Exception ex)
    {
        MessageBox.Show($"Erro: {ex.Message}");
    }
};
1
Pesquisa/teste
Programação Assíncrona, nível 59, lição 4
Indisponível
Programação Assíncrona
Assincronia vs. Multithreading
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION