async void có ba khác biệt so với async Task, cả ba đều bất lợi:
1. Không await được — caller không có Task để chờ, nên method có thể vẫn đang chạy khi caller đã đi tiếp hoặc khi request đã kết thúc.
2. Exception không bắt được bằng try/catch thông thường — với async Task, lỗi được gói vào Task; với async void, lỗi được ném thẳng lên SynchronizationContext đang hoạt động, thường dẫn tới crash process.
3. Không test được — không có gì để await trong unit test.
async void FireAndForget() => throw new InvalidOperationException();
try { FireAndForget(); }
catch (Exception) { /* never reached — process may crash */ }Ngoại lệ hợp lệ duy nhất: event handler, vì delegate của event có chữ ký trả void.
Ngay cả khi đó vẫn nên bọc toàn bộ thân handler trong try/catch để lỗi không thoát ra ngoài.
private async void OnSaveClicked(object sender, EventArgs e)
{
try { await SaveAsync(); }
catch (Exception ex) { _logger.LogError(ex, "save failed"); }
}Còn lại luôn trả Task.
Nếu thật sự cần fire-and-forget trong web API thì dùng cơ chế có kiểm soát (hàng đợi, BackgroundService, hoặc IHostApplicationLifetime) chứ không thả async void.