Skip to main content

Porque você deveria evitar o async void no C# | Artigo .NET

Async void: o vilão oculto que quebra testes, esconde exceções e atrapalha o debug


Ainda espalhando async void pelo seu código como queijo ralado no macarrão?

Acha que “um pouquinho a mais não faz mal”? Spoiler: essa combinação inocente de palavras-chave pode causar exceções silenciosas, sumir com stack traces e transformar sua sexta-feira tranquila em uma caça a bugs de pesadelo.


O Problema em Poucas Palavras

async/await é uma das maiores evoluções de produtividade do C#, mas o momento em que você combina async com void é exatamente quando você desliga os dois superpoderes da feature:

  1. Propagação de exceções — o runtime não consegue retornar o erro ao chamador se não há um Task a ser aguardado.
  2. Composição — sem um Task retornado, quem chama o método não pode usar await ou encadear operações, tornando o controle de fluxo impossível.

async void basicamente significa “dispare e esqueça” — ótimo para fogos de artifício, péssimo para código de produção.


Por que async void existe, afinal?

Ele foi criado apenas para compatibilidade com manipuladores de eventos (event handlers):

private async void OnButtonClick(object? sender, RoutedEventArgs e)
{
await ViewModel.SaveAsync();
}

Frameworks de interface gráfica (WinForms, WPF, Xamarin, MAUI) dependem de delegates que retornam void, como EventHandler. O async void permite inserir await dentro desses handlers sem reescrever toda a camada de UI. Fora desse contexto, é um code smell.


5 Perigos Ocultos do async void

1. Exceções não capturadas derrubam o app

public async void DoWork()
{
throw new InvalidOperationException("Boom!");
}

Como não há Task, essa exceção pula o seu try/await/catch normal e vai direto para o pipeline global de UnhandledException. No ASP.NET Core, isso pode encerrar a requisição; em apps desktop, pode crashar o aplicativo.


2. Contexto de erro perdido

Uma exceção lançada dentro de um async void ainda aparece eventualmente — mas sem o stack trace completo.

public async void SaveCustomerAsync(Customer customer)
{
await _repository.SaveAsync(customer);
}

Quando o erro chega ao manipulador global, tudo que você vê é SaveCustomerAsync e o código interno do runtime. Compare com a versão correta:

public async Task SaveCustomerAsync(Customer customer) =>
await _repository.SaveAsync(customer);

// Chamada
await _customerService.SaveCustomerAsync(cmd.Customer);

Agora, o stack trace inclui o método, o serviço e o controlador que iniciou a operação — exatamente as pistas que você precisa para debugar.


3. Condições de corrida (race conditions) “dispare e esqueça”

Executar algo que parece síncrono mas não é pode causar bugs de ordem sutil:

public async void CleanupAsync()
{
await _cache.RemoveAsync(_key);
_logger.LogInformation("Cache limpo");
}

// Em outro ponto
CleanupAsync(); // retorna imediatamente
_responseCache.MarkComplete(); // executa **antes** de RemoveAsync terminar

O objeto é marcado como “imutável” antes de o cache realmente ser limpo — e horas depois você está caçando o bug fantasma. Solução: retorne um Task e use await, ou mova a execução para uma fila supervisionada.


4. Pesadelo nos testes

Testes unitários são naturalmente síncronos — eles precisam saber quando o sistema sob teste terminou.

// Código de produção
static async void SendInvoiceEmailAsync(Order order) =>
await _mailer.SendAsync(order);

// Teste
SendInvoiceEmailAsync(order);
Assert.True(_mailer.WasSent); // falha intermitente

O Assert roda antes do envio terminar. Você acaba adicionando Thread.Sleep ou Task.Delay, o que não garante determinismo.

A forma correta:

static async Task SendInvoiceEmailAsync(Order order) =>
await _mailer.SendAsync(order);

// Teste
await SendInvoiceEmailAsync(order);
Assert.True(_mailer.WasSent);

5. Concorrência fora de controle

Cada método async void posta sua continuação no SynchronizationContext atual. Em frameworks de UI, esse contexto é single-threaded — se você enfileira muitos, o app congela.

private async void OnKeyPress(object? sender, KeyEventArgs e)
{
await Task.Delay(50);
_textbox.Text += e.KeyChar;
}

Pressionar várias teclas rapidamente cria centenas de continuations, travando a interface. Retorne um Task, use await corretamente ou delegue o trabalho pesado a Task.Run.


Refatorando: antes e depois

Antes:

// Ruim
public async void ProcessOrder(int orderId)
{
await _repository.SaveAsync(orderId);
}

Depois:

// Correto
public async Task ProcessOrderAsync(int orderId)
{
await _repository.SaveAsync(orderId);
}

// Em outro ponto
await _orderService.ProcessOrderAsync(id);

Agora os chamadores podem coordenar, reexecutar ou compor as operações com segurança.


Como caçar async void na sua base de código

  • 🔍 Roslyn Analyzers — ative o AsyncFixer ou a regra VSTHRD100 dos analisadores da Microsoft.
  • 🧭 Busca no Visual StudioCtrl + Shift + F por async void (diferenciar maiúsculas/minúsculas).
  • 🧹 dotnet format / analyzers — trate async void fora de eventos como erro.
  • 🧾 Checklist de code review — rejeite PRs com async void que não sejam event handlers.

Adicione isto ao seu .editorconfig:

# Proibir async void exceto em event handlers
csharp_style_async_void_methods = false:error

Conclusão: fuja do void, abrace o Task

async void é uma armadilha disfarçada de açúcar sintático — aceitável apenas em handlers de eventos, mas fatal em qualquer outro lugar. Troque por async Task, use await corretamente, e seu “eu do futuro” (e seu time de QA) vão te agradecer.