自定义异常类必须继承exception,覆盖三个构造函数并调用base(...);命名需带exception后缀且体现领域意图;统一处理应使用useexceptionhandler中间件而非基类try/catch;二次抛出须用throw而非throw ex以保留原始堆栈。

自定义异常类必须继承 Exception 或其子类
直接继承 Exception 是最稳妥的做法,别图省事去继承 SystemException 或其他中间类——它们有框架语义,乱用会导致异常分类混乱。构造函数至少得覆盖三个常用重载:Exception()、Exception(string message)、Exception(string message, Exception innerException)。缺了带 innerException 的那个,链式异常信息就断了,线上查问题时会丢关键上下文。
常见错误现象:抛出后用 ex.ToString() 看不到原始异常堆栈;或全局异常捕获里 ex.InnerException 为 null,但实际业务逻辑里明明传了。
- 务必在每个构造函数里调用
base(...),别漏掉 - 不要在异常类里加 public 属性或业务字段——序列化和跨 AppDomain 时可能失败
- 如果真需要附带业务码,用只读属性 +
protected set,并在构造中赋值
业务异常命名要带 Exception 后缀且体现领域意图
比如 InsufficientBalanceException 比 BalanceException 或 BalanceError 更准确;OrderNotFoundException 明确表达“查不到”,而不是模糊的 OrderInvalidException(后者更适合校验失败场景)。.NET 生态默认约定所有异常类型名以 Exception 结尾,不遵守会影响工具识别(如 IDE 快速修复、静态分析规则)。
使用场景:API 接口层做参数校验失败时 throw InvalidParameterException,仓储层查不到记录时 throw ProductNotFoundException,这两者不该混用——统一异常处理中间件靠类型区分响应状态码。
- 避免泛化命名:如
BusinessException、AppException,等于没分 - 不要用动词开头:如
ThrowOrderException,这是方法名习惯 - 英文名保持 PascalCase,别写成
insufficient_balance_exception
统一异常处理不能只依赖 try/catch 全局包裹
ASP.NET Core 里真正可靠的是 UseExceptionHandler 中间件 + 自定义 ExceptionHandler<t></t>,而不是在 Controller 基类里写一个万能 try/catch。前者能捕获未处理异常、支持异步上下文、可注入服务;后者容易漏掉过滤器、模型绑定、中间件异常,且无法处理 Task 内部未 await 的异常。
性能影响:中间件方式只在真正出错时执行,而基类 try/catch 是每次请求都进一层 try 块,CLR 异常机制本身无开销,但滥用 catch 会干扰 JIT 优化路径。
-
UseExceptionHandler配置后,确保app.UseRouting()在它之前,否则路由异常进不来 - 在 handler 里别直接
throw ex,要用context.Response.StatusCode = 500显式设状态码 - 若需记录日志,从
context.Features.Get<iexceptionhandlerpathfeature>()</iexceptionhandlerpathfeature>拿原始路径,别依赖HttpContext.Request.Path
throw 和 throw ex 的区别直接影响异常链完整性
在 catch 块里二次抛出时,用 throw(无参数)保留原始堆栈;用 throw ex 会重置堆栈起点到当前行,导致上层看到的异常位置是“重新抛出处”而非“最初发生处”。这是线上排障最常踩的坑——日志里显示异常在 GlobalExceptionHandler.Invoke 里,其实根源在 Repository 的第 3 行 SQL 执行。
兼容性注意:C# 6+ 支持异常过滤器(catch (MyException ex) when (ex.Code == 1001)),此时仍该用 throw 继续向上抛,别误写成 throw ex。
- 想补充信息?用
throw new MyException("额外说明", ex),把原异常当innerException - 不要为了“加日志”而在 catch 里
throw ex,日志写完直接throw - 异步方法里
await后的throw依然保持原始堆栈,放心用
异常类型的粒度和 throw 时的堆栈控制,比怎么写 handler 逻辑更关键。很多人花大力气写统一返回格式,却在第一处 throw ex 就把根因藏没了。











