async void 方法不可被 await 且异常无法捕获,仅限 UI 事件处理器使用;其他场景必须改用 async Task 并正确 await,否则将导致控制权丢失、UI 错乱或应用崩溃。
你写 async void OnClick(),别人在别处想 ?编译器直接报错:
await OnClick()Cannot await 'void'。这不是语法糖问题,是语言设计上就切断了等待链路。这意味着:
.Wait()、.GetAwaiter().GetResult() 等方式同步等待(会死锁或抛异常)比如你在 Blazor 中绑定按钮事件,返回 void,组件会在点击后立刻刷新 UI;而返回 Task,Blazor 会等异步操作完成再调用 StateHasChanged() —— 这个差异常导致 UI 状态错乱。
这是最危险的一点:async void 里未捕获的异常,不会进入 Task 的异常容器,而是直接扔到当前 SynchronizationContext(比如 UI 线程或 ASP.NET Core 的请求上下文),如果没全局处理,应用可能直接崩溃。
对比:
async Task BadTask() => throw new InvalidOperationException("Boom!");
// 调用方可捕获:try { await BadTask(); } catch (Exception e) { ... }
async void BadVoid() => throw new InvalidOperationException("Boom!");
// 调用方 try-catch 完全无效,异常飞走,进程可能终止
尤其在 ASP.NET Core 中,async void 导致的未处理异常会让整个请求无声失败,日志里只有一行 Unhandled exception,排查成本极高。
仅限于 **UI 事件处理器**,且必须满足两个条件:
Button.Click += (s,e) => { }、WPF 的 RoutedEventHandler、Blazor 的 @onclick)void,你无法改写(例如不能把 EventHandler 改成 Func)其他所有情况都应避免:
async void
Timer.Elapsed 回调里用(应改用 Task.Run + async Task 包装)Array.ForEach 或 LINQ 的 lambda 里写 async void(会静默丢失异常)当你发现一个 async void 方法本不该存在,改成 async Task 后,往往要连带改三处:
async void DoWork() → async Task DoWork()
DoWork(); → await DoWork();(注意调用方法本身也得是 async)@onclick="async () => await DoWork()" 这种包装写法如果旧事件签名强制 void(比如 WinForms),至少在内部加 try/catch 并记录日志,别让它裸奔:
private async void button1_Click(object sender, EventArgs e)
{
try
{
await DoWorkAsync();
}
catch (Exception ex)
{
MessageBox.Show($"操作失败:{ex.Message}");
// 或写入 ILogger
}
}
真正难的不是写对 async Task,而是意识到:一旦用了 async void,你就主动交出了错误传播路径和执行生命周期的控制权——而这往往是线上事故的第一块松动的螺丝。