1. 介绍
先从一个重要的问题开始:为什么不能单纯信任异步方法并希望它们总是顺利完成?对文件的操作经常会抛出异常——文件可能被删除,磁盘空间用完,权限不足或者文件被锁住。在同步代码里你会在熟悉的 try-catch 块里捕获这些问题。异步代码的哲学是一样的,但有细微差别:错误可能不会在调用方法时立即发生,而是在操作实际执行的时候发生。
错误发生的时刻
在同步代码里,通过 StreamReader.Read() 读取时异常会直接在调用那一行抛出——你在 catch 里捕获它,这样很好。
在异步代码中(await stream.ReadAsync())错误不会在操作启动时发生,而是在你对任务做 await 时——任务以错误结束的时候。如果忘了写 await,错误可能在一段时间内对你“不可见”。
2. 如何在异步方法里捕获异常
先看一个常见的模板:
try
{
using FileStream fs = new FileStream("myfile.txt", FileMode.Open);
byte[] buffer = new byte[1024];
int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length);
// 进一步处理...
}
catch (IOException ex)
{
Console.WriteLine("输入输出错误: " + ex.Message);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("没有访问文件的权限: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("未知错误: " + ex);
}
对,就是用熟悉的 try-catch,但在异步方法里。重要的是方法本身要有关键字 async,否则编译器会抱怨。
重要细节:把 await 放在哪里
Task<int> readTask = fs.ReadAsync(buffer, 0, buffer.Length);
// ... 在这里意外地忘了 await 或者处理
在这种情况下,如果发生错误,它会进入任务本身(Task),你不会知道,直到你尝试获取结果——比如通过 await readTask 或者查看 Task.Exception。如果你完全忘记了 await ——任务可能以错误结束,但没人会告诉你。
3. 为什么不处理错误会让异步代码变得狡猾
场景 1: «Fire and forget» — 新手的陷阱
FileStream fs = new FileStream("file.txt", FileMode.Open);
byte[] buffer = new byte[8000];
fs.ReadAsync(buffer, 0, buffer.Length);
// 然后程序继续自己的事情
读取操作会去“并行跑”,如果它以错误结束,没有任何 catch 能捕获它。异常会藏在任务里。这个模式叫「fire and forget」,在真实应用中可能导致关键错误丢失和资源泄露。
场景 2: 异步方法没有 await
Task t = MyAsyncMethod();
// ... 做点其他事,然后忘了 t
在 MyAsyncMethod 里发生的错误不会冒出来,除非你显式等待那个任务(await t 或者 t.Wait())。
4. 如何正确捕获来自异步任务的错误
策略 1: 始终使用 await
try
{
await SomeFileOperationAsync();
}
catch (Exception ex)
{
Console.WriteLine("出问题了: " + ex.Message);
}
这样异常会在等待点直接抛出,不会丢失。
策略 2: 用 .ContinueWith 来处理
如果因为某些原因你不想用 await,可以通过 ContinueWith 添加错误处理:
var task = fs.ReadAsync(buffer, 0, buffer.Length);
task.ContinueWith(t =>
{
if (t.Exception != null)
Console.WriteLine("异步读取时出错: " + t.Exception.InnerException);
}, TaskContinuationOptions.OnlyOnFaulted);
坦白说,在现代 C# 应用中很少这样做——async/await 使代码更简单、更清晰。
异步文件操作时可能遇到的异常
- IOException — 磁盘故障、文件未找到、路径过长、设备不可用。
- UnauthorizedAccessException — 权限不足。
- ObjectDisposedException — 在操作完成前流被关闭了。
- OperationCanceledException — 通过取消令牌(CancellationToken)取消了操作。
5. 示例:带错误处理的异步读取
把这些逻辑加到你的程序里:
using System;
using System.IO;
using System.Threading.Tasks;
namespace FileAsyncDemo
{
class Program
{
static async Task Main()
{
string path = "bigfile.txt";
byte[] buffer = new byte[4096];
try
{
using FileStream fs = new FileStream(path, FileMode.Open);
int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length);
Console.WriteLine($"已从 {path} 读取 {bytesRead} 字节");
}
catch (FileNotFoundException ex)
{
Console.WriteLine("未找到文件: " + ex.Message);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("没有文件访问权限: " + ex.Message);
}
catch (IOException ex)
{
Console.WriteLine("读/写错误: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("其他错误: " + ex.Message);
}
}
}
}
重要提示
如果在 Main 中你不使用 await,而只是做 Task result = SomeAsyncMethod(); ——错误会“静默”存在,直到你真正尝试获取结果时才显现。
6. 最常见的错误和踩坑
忘记对异步方法使用 await ——错误不会及时冒出来,程序行为变得不可预测。
不把异步调用包在 try-catch 中 ——应用在第一次失败时就崩溃。
只捕获 Exception,忽略像 UnauthorizedAccessException 或 OperationCanceledException 这些特定异常 ——诊断变差。
使用「fire and forget」任务而不记录错误 ——异常会丢失在 Task 里。
以为任务完成就表示成功。需要显式等待结果(await)并处理异常。
GO TO FULL VERSION