Porque você deveria evitar o async void no C# | Artigo .NET
-
Porque você deveria evitar o
async voidno C# | Artigo .NET -
Artigo: https://medium.com/net-code-chronicles/why-you-should-avoid-async-void-in-csharp-ead37e3e5894
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:
- Propagação de exceções — o runtime não consegue retornar o erro ao chamador se não há um
Taska ser aguardado. - Composição — sem um
Taskretornado, quem chama o método não pode usarawaitou 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
AsyncFixerou a regraVSTHRD100dos analisadores da Microsoft. - 🧭 Busca no Visual Studio —
Ctrl + Shift + Fporasync void(diferenciar maiúsculas/minúsculas). - 🧹 dotnet format / analyzers — trate
async voidfora de eventos como erro. - 🧾 Checklist de code review — rejeite PRs com
async voidque 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.