catch 里不写 throw 或 throw ex 会吞掉异常
很多开发者在
catch块里只做日志就完事,比如:
try { DoSomething(); }
catch (Exception ex) {
Log.Error(ex.Message);
}这会导致调用栈被截断,上层完全不知道发生了什么。更糟的是,如果后续逻辑依赖异常传播(比如事务回滚、重试机制),程序会静默失败。
正确做法是:需要处理就
throw;(原异常重抛),需要包装就
throw new CustomException("msg", ex);。绝对避免 throw ex;——它会清空原始堆栈信息。
throw;:保留原始堆栈,推荐用于“记录后继续上抛”
throw new Exception(..., ex);:带内嵌异常,适合封装领域错误
throw ex;:删掉堆栈,调试时找不到源头,禁用
finally 不保证执行,但 try/catch/finally 结构本身是安全的
finally块在绝大多数情况下都会运行,包括
return、
break、
continue甚至
throw出当前方法时。但它不是绝对可靠的——比如进程被强制终止(
Environment.FailFast、
Thread.Abort已废弃但仍有类似场景)、StackOverflow、OutOfMemory,或 Windows 上的硬关机。
所以
finally适合做资源清理(如
stream.Close()、
conn.Dispose()),但不适合放关键业务逻辑(比如发通知、写审计日志)——这些应该放在
try或
catch中并单独容错。 用
using替代手动
finally关闭资源,更简洁且编译器保障 如果必须手写
finally,优先调用
Dispose()而非
Close()(后者可能不释放所有资源) 不要在
finally里写可能抛异常的代码,否则会覆盖原异常
多个 catch 块顺序错乱会导致子类异常永远捕不到
C# 的
catch是从上到下匹配的,一旦某个
catch满足类型条件就进入,不再检查后面的。所以如果把基类异常(如
Exception)写在子类(如
ArgumentNullException)前面,子类永远没机会触发。
try { ... }
catch (Exception ex) { /* 这里会捕获所有异常 */ }
catch (ArgumentNullException ex) { /* 永远不会进来 */ }编译器其实会报错:CS0160 “先前已捕获到一个更加具体的异常类型”,但有些旧项目或动态编译场景可能绕过检查。
总是按“从具体到宽泛”排列:先ArgumentNullException,再
IOException,最后
Exception不要为了“省事”只留一个
catch (Exception),丢失异常语义 如果想统一处理,用
catch (Exception ex) when (ex is ArgumentNullException || ex is InvalidOperationException)来组合条件
async 方法里 try/catch 不能直接捕获 await 后的异常
在
async Task方法中,
await后抛出的异常会被包装进
Task,不会直接冒泡到同步的
catch块——除非你
await它。下面这段代码里的
catch根本抓不到
DoAsyncWork()抛的异常:
try {
DoAsyncWork(); // 忘了 await!返回 Task 后立即往下走
}
catch (Exception ex) { /* 不会执行 */ }正确写法必须
await:
try {
await DoAsyncWork();
}
catch (HttpRequestException ex) { /* 这里才能捕获 */ }
未 await的异步调用等于“fire and forget”,异常会留在
Task.Exception里,最终可能触发
TaskScheduler.UnobservedTaskException如果要在非 await 场景下捕获,得显式
.Wait()或
.Result(但会阻塞线程,不推荐) ASP.NET Core 中未处理的异步异常常导致 500 且无日志,务必检查所有
await是否遗漏
真正难的不是语法,是判断该不该吞异常、在哪一层转化异常语义、以及 async 下异常是否真的落地了。这些地方一松懈,问题就藏进日志死角。
