必须 await 才能捕获 async 方法中的异常,否则异常被吞掉或触发 UnobservedTaskException;try/catch 需直接包围 await 表达式,async void 是异常黑洞,仅限顶层事件处理器。

async 方法里不加 await 会丢失异常
调用 async 方法但没用 await,比如直接写 DoSomethingAsync();,异常不会抛到当前上下文,而是被吞掉或触发 TaskScheduler.UnobservedTaskException(.NET 5+ 默认静默丢弃)。这会让错误难以定位。
- 必须
await才能捕获异常,或显式调用.Wait()/.Result(不推荐,可能死锁) - 若需“火后即忘”,至少用
_= DoSomethingAsync().ContinueWith(t => { if (t.IsFaulted) Log(t.Exception); }); - 单元测试中容易漏掉这个点:没
await的异步调用,Assert.Throws会失败
try/catch 包裹 await 表达式才有效
try/catch 必须直接围住 await 调用,而不是整个方法体或 async 声明处。因为 async 方法返回的是 Task,异常实际封装在该 Task 中,只有 await 才会解包并重抛。
public async Task ProcessAsync()
{
try
{
// ✅ 正确:await 在 try 内,异常可被捕获
await File.ReadAllTextAsync("missing.txt");
}
catch (FileNotFoundException ex)
{
Log(ex);
}
}
public async Task ProcessAsyncBad()
{
// ❌ 错误:异常发生在 await 之后,但 try 没包住它
var task = File.ReadAllTextAsync("missing.txt");
try
{
await task; // 异常仍在此抛出,但 try 已结束?不,这行还在 try 内 —— 关键是别把 await 和 try 拆开
}
catch (FileNotFoundException) { ... }
}
多个 await 连续调用时,每个都可能抛异常
一个 async 方法里有多个 await,每个都应单独考虑异常路径。不能假设前一个成功,后一个就一定安全。
-
await后的代码属于“延续(continuation)”,一旦前面的Task出错,后续语句根本不执行 - 常见误写:
var data = await GetDataAsync(); var result = await ProcessAsync(data);—— 若ProcessAsync抛异常,data已获取但无法清理,需考虑using或try/finally - 若需“尽力而为”,可用
Task.WhenAll(...).ContinueWith(...)分离错误处理逻辑,但语义更重
async void 是异常黑洞,仅限事件处理器
async void 方法中的异常无法被常规 try/catch 捕获,会直接崩掉当前同步上下文(如 UI 线程),且无法通过 Task 观察。只允许用于真正顶层事件,例如 WPF 的 Button.Click。
private async void OnButtonClick(object sender, RoutedEventArgs e)
{
try
{
await LoadDataAsync(); // ✅ 可以,但异常会触发 AppDomain.UnhandledException
}
catch (Exception) { /* ❌ 不会被执行 */ }
}
private async Task OnButtonClickGood(object sender, RoutedEventArgs e)
{
try
{
await LoadDataAsync(); // ✅ 推荐:返回 Task,调用方可 await + catch
}
catch (Exception ex)
{
ShowError(ex.Message);
}
}
复杂点在于:异步流中异常不是“同步抛出”,而是延迟到 await 解包时才浮现;很多调试器默认不中断未处理的异步异常,得手动开启“Common Language Runtime Exceptions > Thrown”才能看到。










