throw是强制中断执行流的操作,未捕获则崩溃;应避免用throw控制流程,catch中用throw;保留原始堆栈,async中异常延迟暴露,自定义异常须实现三个构造函数。

throw 在 C# 中不是“可选动作”,而是立即中断当前执行流的强制操作;没人捕获就崩溃,不是静默失败。
throw new Exception() 会立刻终止当前方法
只要执行到 throw 语句,不管它写在 if 分支里还是方法末尾,当前方法剩余代码全部跳过,控制权交还给调用栈上最近的 try 块。没匹配的 catch?进程直接退出(控制台程序)或线程终止(后台线程)。
- 常见错误:在工具类里写
throw new ArgumentException("x is null"),但调用方既没包try-catch,也没检查参数合法性——结果就是上线后偶发崩溃 - 正确姿势:把校验逻辑和异常抛出绑定在契约明确的方法中,比如
ParseInt(string input)内部做空值判断再抛ArgumentNullException - 别用
throw控制流程:比如在循环里靠throw new InvalidOperationException()跳出,这会让堆栈混乱、性能差,且静态分析工具无法识别意图
catch 里用 throw; 还是 throw ex;?只看调试要不要原始堆栈
throw; 和 throw ex; 看似一样,实际影响极大:前者保留从最初抛出处开始的完整堆栈,后者只从这一行开始记录新堆栈。
- 线上查问题时,
throw ex;会让日志里只看到“在 Logger.cs 第 42 行重抛”,完全丢失真正出错位置(比如DataAccess.cs第 15 行的空引用) - 标准写法是
catch (Exception ex) { Log(ex); throw; }—— 日志记全,堆栈留全 - 如果真要包装异常(比如加业务上下文),应该用
throw new MyBusinessException("下单失败", ex);,而不是throw ex;
async Task 方法里 throw 的行为和同步方法不同
在 async Task 方法中 throw 不会立刻崩线程,而是让返回的 Task 进入 Faulted 状态;异常被封装进 task.Exception,直到 await 才真正暴露出来。
- 这意味着:不
await就直接丢弃Task,异常会被吞掉(.NET 6+ 会触发UnobservedTaskException事件,但默认不处理) - 测试时容易漏:写个
async Task ThrowAsync() { throw new InvalidOperationException(); },但只调用不await,看起来“没报错”——其实只是延迟暴露 - 若需在未 await 前检查异常,得先判断
task.IsFaulted,再读task.Exception.InnerException(注意不是task.Exception,因为外层是AggregateException)
自定义异常类必须实现三个构造函数
只写 class MyException : Exception 是不够的。缺少标准构造函数会导致跨 AppDomain 失败、反序列化异常、调试器无法显示原始上下文。
- 必须提供:
public MyException()、public MyException(string message)、public MyException(string message, Exception innerException) - 推荐加
[Serializable]特性,尤其在 Remoting 或旧版 WCF 场景下 - 避免在自定义异常里存非序列化字段(如
FileStream、委托),否则反序列化时直接抛SerializationException
最常被忽略的一点:throw 的传播路径依赖完整的调用栈链路,任何一环用了 throw ex; 或漏了 await,都会让异常“断连”。调试时找不到源头,八成是这里出了问题。










