The Error
Random 500 errors. Logs show:
System.InvalidOperationException: An asynchronous module or handler completed while an asynchronous operation was still pending.
Quick Fix - 1 Minute
Change async void to async Task.
Wrong:
[HttpGet]
public async void GetData() { await _service.DoWorkAsync(); }
Right:
[HttpGet]
public async Task<IActionResult> GetData()
{
await _service.DoWorkAsync();
return Ok();
}
Why This Happens
ASP.NET can't await void. It thinks the request finished and sends response. Your await runs after, so exceptions crash the app. Never use async void except event handlers.
Real-World Scenario: Fire-and-Forget in Controller
#1 cause of prod crashes. Dev tries to "not block" the request:
// WRONG: async void - Request ends before work finishes
[HttpPost("/orders")]
public async void CreateOrder(OrderDto dto)
{
await _db.Orders.AddAsync(dto); // Dev Fix: This throws after response sent
await _db.SaveChangesAsync();
_emailService.SendAsync(dto.Email); // Exception here crashes entire app
}
// RIGHT: Return Task and use background queue for long work
[HttpPost("/orders")]
public async Task<IActionResult> CreateOrder(OrderDto dto)
{
await _db.Orders.AddAsync(dto);
await _db.SaveChangesAsync();
// Dev Fix: Queue long work, don't await it
_backgroundQueue.QueueBackgroundWorkItem(async token =>
{
await _emailService.SendAsync(dto.Email, token);
});
return Accepted(); // 202 - "I'll process this"
}
// For .NET 8: Use IHostApplicationLifetime + Channel<T>
public interface IBackgroundTaskQueue
{
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
Task<Func<CancellationToken, Task>> DequeueAsync(CancellationToken token);
}
Why async void kills apps: ASP.NET Core has a SynchronizationContext. When your controller returns, it disposes the request. Any async void continuation runs on disposed context = crash. async Task lets ASP.NET track the operation.
Related Fixes You Should Know
async void bugs create these downstream issues:
- Async/Await Deadlock - You removed
async voidbut added.Resultor.Wait(). Now you deadlock onSynchronizationContext. Alwaysawait. - TaskCanceledException HttpClient - Request ends due to
async void, HttpClient throwsTaskCanceledException. Fix return type first. - BackgroundService CancellationToken - Correct way to do fire-and-forget. Don't use
async void, useIHostedService+ queue. - Request Pipeline Not Awaited - Custom middleware with
async voidbreaks entire pipeline. Middleware must returnTask.
FAQ
Q: When is async void actually OK?
Only for event handlers: button_Click, OnStartup. Never in ASP.NET Core, libraries, or anything that returns to a framework. Event handlers are top-level, so no one awaits them anyway.
Q: How do I do fire-and-forget without async void?
Use IHostedService + Channel<T> or Hangfire. Return 202 Accepted to client immediately. Queue the work. Never async void or Task.Run() without tracking.
Common Scenarios
- Controller actions: Must return
TaskorTask<IActionResult> - Service methods: If called with
await, must returnTask - Fire-and-forget: Don't. Use
IHostedServiceor background queue
Best Practice
Install Microsoft.VisualStudio.Threading.Analyzers. It marks async void as compile error. Add Async suffix: SaveAsync().
No comments yet. Be the first to share your thoughts!