c# 异常处理 try catch finally

应避免在 catch 中仅记录日志而不重抛异常,否则会截断调用栈并导致静默失败;正确做法是使用 throw; 保留堆栈,或 throw new CustomException("msg", ex) 封装异常,禁用 throw ex;。

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 块在绝大多数情况下都会运行,包括 returnbreakcontinue 甚至 throw 出当前方法时。但它不是绝对可靠的——比如进程被强制终止(Environment.FailFastThread.Abort 已废弃但仍有类似场景)、StackOverflow、OutOfMemory,或 Windows 上的硬关机。

所以 finally 适合做资源清理(如 stream.Close()conn.Dispose()),但不适合放关键业务逻辑(比如发通知、写审计日志)——这些应该放在 trycatch 中并单独容错。

  • 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 下异常是否真的落地了。这些地方一松懈,问题就藏进日志死角。