CodeGym /Cursos /C# SELF /Exceções em tarefas "fire and forget"

Exceções em tarefas "fire and forget"

C# SELF
Nível 61 , Lição 0
Disponível

1. O que é "fire and forget"?

Em programação o termo fire and forget significa iniciar uma tarefa sem esperar que ela termine. No mundo C# e .NET isso normalmente é feito com tarefas Task que são iniciadas, mas não aguardadas (await), sem manter referência e efetivamente esquecidas.

// O botão inicia uma tarefa em background, mas em nenhum lugar ela é await-ada.
button.Click += (s, e) =>
{
    Task.Run(() => DolgayaOperaciya());
};

Parece atraente: "deixa rodar em background enquanto eu faço outra coisa". Mas com essa abordagem, se ocorrer uma exceção dentro da tarefa, ninguém vai saber a tempo — ela some silenciosamente.

2. Como funciona o tratamento de exceções em Task

Clássico: await e tratamento de erros

A maneira padrão de trabalhar com tarefas assíncronas é via await. Se houver um erro na tarefa, ele será lançado no ponto do await:

try
{
    await SomeOperationAsync(); // se aqui dentro lançar Exception, ela cairá no catch
}
catch(Exception ex)
{
    Console.WriteLine("Ops! A tarefa lançou um erro: " + ex.Message);
}

Ou seja, quando você espera a conclusão da tarefa, você não perde a exceção.

Mas tarefas "fire and forget" não são aguardadas!

public void ZapustitBezOzhidaniya()
{
    // A tarefa roda por si só. Ninguém a espera...
    Task.Run(() => {
        // Em algum ponto ocorre problema:
        throw new InvalidOperationException("Ih, deu ruim!");
    });
    // O método terminou, a tarefa roda silenciosamente em background.
}

Se ocorrer uma exceção dentro dessa tarefa, ela não será lançada no thread principal. A aplicação continua rodando como se nada tivesse acontecido.

Importante

No .NET, uma tarefa com exceção não tratada fica no estado Faulted. Mas se você não a aguarda (await, .Result, .Wait() etc.), ninguém vai ler a exceção e ela não vai aparecer no código chamador.

O que realmente acontece "por baixo do capô"?

Para tarefas que ninguém aguarda, resta uma única chance de serem notadas — o evento TaskScheduler.UnobservedTaskException. Ele dispara quando o garbage collector (GC) encontra uma tarefa com exceção não observada. Mas isso não acontece imediatamente e nem onde você espera — não dá pra confiar nisso.

3. Demonstração: erro fire-and-forget

// Exemplo: iniciamos uma tarefa fire-and-forget direto do Main
using System;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        FireAndForgetExample();

        Console.WriteLine("Thread principal continua rodando...");
        // Damos um tempo para a tarefa terminar
        Task.Delay(2000).Wait();
    }

    static void FireAndForgetExample()
    {
        Task.Run(() =>
        {
            Console.WriteLine("Tarefa fire-and-forget começou!");
            Task.Delay(500).Wait();
            throw new InvalidOperationException("Erro dentro da tarefa fire-and-forget!");
        });
    }
}

Se você rodar esse código, ... nada de especial vai acontecer. O erro acontece, mas o programa não fica sabendo. Às vezes um aviso aparece na Output Window da IDE, mas o usuário não recebe informação.

Por que isso é perigoso em projetos reais?

  • Bugs complexos que são difíceis de reproduzir ("às vezes não funciona — não sei por quê").
  • Perda silenciosa de dados ou lógica (por exemplo, um email que não foi enviado).
  • Em produção — falta de sinais sobre problemas, a menos que exista logging configurado.

4. Maneiras corretas de tratar erros em fire-and-forget

Log e tratamento de erros dentro da própria tarefa

O nível mínimo seguro é capturar exceções dentro da própria tarefa fire-and-forget:

Task.Run(() =>
{
    try
    {
        // Seu código longo/perigoso
        throw new InvalidOperationException("Algo deu errado!");
    }
    catch (Exception ex)
    {
        // Loga o erro ou informa o usuário
        Console.WriteLine("Fire-and-forget: exceção capturada: " + ex.Message);
        // Pode escrever em um arquivo de log, enviar para sistema de alerta etc.
    }
});

Métodos assíncronos void (e por que não é uma boa)

async void DangerousFireAndForget()
{
    // Algo perigoso
    throw new Exception("Boom!");
}

Métodos async void são, na prática, fire-and-forget: não dá pra aguardá-los, eles não retornam Task. Exceções vindas deles vão para o handler global da aplicação (por exemplo, AppDomain.UnhandledException) e muitas vezes causam a queda do processo. Use async void só para event handlers — e mesmo assim com cuidado.

Usando helpers para tratar erros

É conveniente extrair o lançamento seguro de fire-and-forget para um wrapper:

// Método genérico para iniciar safe fire-and-forget
public static void RunSafeFireAndForget(Func<Task> taskFactory)
{
    Task.Run(async () =>
    {
        try
        {
            await taskFactory();
        }
        catch (Exception ex)
        {
            // Loga a exceção
            Console.WriteLine("Fire-and-forget (safe): " + ex);
            // Pode enviar para sistema de monitoramento!
        }
    });
}

// Uso:
RunSafeFireAndForget(async () =>
{
    await Task.Delay(1000);
    throw new InvalidOperationException("Dentro do fire-and-forget!");
});

Exemplo do "mundo real": envio de email

// Botão de enviar email:
private void buttonSend_Click(object sender, EventArgs e)
{
    Task.Run(() => SendEmail());
}

// Método de envio:
private void SendEmail()
{
    try
    {
        // Aqui pode acontecer o envio real
        throw new Exception("Servidor SMTP indisponível!");
    }
    catch (Exception ex)
    {
        // Logging
        File.AppendAllText("errors.log", $"Erro ao enviar: {ex.Message}\n");
    }
}

5. E o UnobservedTaskException?

Como último recurso, o .NET fornece o evento TaskScheduler.UnobservedTaskException. Ele é chamado se uma tarefa terminou com erro, ninguém a aguardou, e o objeto tarefa foi coletado pelo GC. Não confie nisso — é um mecanismo de "última chance".

TaskScheduler.UnobservedTaskException += (sender, e) =>
{
    Console.WriteLine("UnobservedTaskException global: " + e.Exception);
    e.SetObserved(); // Não esqueça de chamar isso, senão a aplicação pode terminar de forma abrupta!
};

Mais detalhes: TaskScheduler.UnobservedTaskException.

6. Nuances úteis

Comparação esquemática de abordagens

Método Exceções tratadas? Onde capturar erros Risco de "perder" erro
await
Sim No código chamador Baixo
Fire-and-forget sem try/catch Não Em nenhum lugar Muito alto
Fire-and-forget com try/catch Sim Dentro da própria tarefa Baixo (se você logar)
async void-method Não (vai para global) Handler global Alto

Como projetar fire-and-forget corretamente

  • Se o resultado ou estado da tarefa é crítico — não faça fire-and-forget. Use await ou guarde a Task para aguardar depois.
  • Fire-and-forget só faz sentido para tarefas realmente não críticas (por exemplo, enviar telemetria).
  • Sempre encapsule fire-and-forget em um método próprio e capture/logue exceções.
  • Para cenários de background mais complexos use filas e workers: Hangfire, Quartz.NET.

Aplicação prática e entrevistas

Em entrevistas frequentemente perguntam: "O que acontece se uma exceção ocorrer em uma tarefa fire-and-forget?" ou "Por que não se deve usar async void sempre?" A resposta certa: você é responsável pelo destino das exceções em tarefas em background — ou captura, loga e analisa, ou recebe bugs-fantasma.

Comparação entre "fire-and-forget" e await

Cenário Confiabilidade no tratamento de erros Aplicabilidade
Await normal Excelente Onde for necessário resultado ou importante saber success/fail
Fire-and-forget Ruim (se não tratar manualmente) Apenas para tarefas realmente background e não críticas
Fire-and-forget com try/catch Bom (se logar) Tarefas background onde não precisa do resultado, mas é importante saber de falhas

Na próxima aula vamos discutir tratamento de erros em tarefas paralelas que retornam múltiplos resultados. Por enquanto, lembre-se: se você atirou em alguma direção, verifique se a bala chegou no alvo!

7. Erros típicos ao trabalhar com tarefas fire-and-forget

Erro nº1: Ignorar exceções em fire-and-forget.
Iniciantes esperam que exceções "vão subir" sozinhas. Sem try-catch e logging elas se perdem, causando bugs indetectáveis.

Erro nº2: Usar async void fora de event handlers.
Esses métodos jogam exceções no handler global (por exemplo, AppDomain.UnhandledException), o que pode encerrar a aplicação abruptamente.

Erro nº3: Tratar exceções demais.
Capturar todas as exceções dentro da tarefa pode esconder problemas que seriam melhor tratados no código chamador, dificultando o debug.

Erro nº4: Ignorar logging.
Sem logging de erros em tarefas fire-and-forget é impossível saber sobre falhas, especialmente em produção.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION