自定义异常类必须继承exception或其子类,需提供三个标准构造函数并添加[serializable]特性;抛出时应传递innerexception,重抛用throw而非throw ex;异常仅用于意外情况,非控制流手段。

自定义异常类必须继承 Exception 或其子类
不继承 Exception 的类无法被 catch 捕获为异常,编译器会报错。常见错误是直接写一个普通类,然后用 throw new MyException() —— 这会导致 CS0155(无法捕获非异常类型)。
推荐继承 Exception,而非 System.ApplicationException(已过时,文档明确不建议使用)。
- 必须提供至少三个构造函数:无参、
string message、string message, Exception innerException - 若需序列化(如跨 AppDomain 或 WCF 场景),需添加
[Serializable]特性 - 避免在构造函数中做耗时操作或依赖外部状态
[Serializable]
public class InsufficientBalanceException : Exception
{
public decimal CurrentBalance { get; }
public decimal RequiredAmount { get; }
public InsufficientBalanceException() : base("账户余额不足") { }
public InsufficientBalanceException(string message) : base(message) { }
public InsufficientBalanceException(string message, decimal current, decimal required)
: base(message)
{
CurrentBalance = current;
RequiredAmount = required;
}
public InsufficientBalanceException(string message, Exception innerException)
: base(message, innerException) { }
protected InsufficientBalanceException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
CurrentBalance = info.GetDecimal("CurrentBalance");
RequiredAmount = info.GetDecimal("RequiredAmount");
}
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("CurrentBalance", CurrentBalance);
info.AddValue("RequiredAmount", RequiredAmount);
}
}
抛出时优先用带业务语义的构造函数
不要只用无参构造,丢失关键上下文。调用方靠 ex.Message 做日志或用户提示时,信息越具体越好;靠属性做程序逻辑分支时(比如重试策略),属性必须被正确初始化。
- 简单场景:用
new InsufficientBalanceException($"余额{cur}不足以支付{req}") - 需要结构化数据参与后续处理:用带参数的构造函数,确保
CurrentBalance和RequiredAmount可靠赋值 - 包装底层异常时,务必传入
innerException,否则堆栈和原始错误信息丢失
throw 与 throw ex 的区别直接影响调试体验
在 catch 块中重新抛出异常时,写 throw ex; 会清空原始堆栈,只剩当前行;而裸 throw; 保留完整原始调用链。这是线上排查最常踩的坑之一。
- 错误写法:
catch (SqlException ex) { throw ex; }→ 堆栈从这里截断 - 正确写法:
catch (SqlException ex) { throw new DataAccessException("数据库操作失败", ex); }(带 inner) - 或仅做日志后透传:
catch (Exception ex) { Log(ex); throw; }(注意不是throw ex;)
自定义异常不应替代返回值或状态码
异常用于“意外”情况,不是控制流手段。比如验证失败(用户名为空)、参数校验不通过,更适合返回 Result<t></t> 或 ValidationResult,而不是抛 InvalidInputException。
- 适合抛异常:文件被占用、网络超时、数据库连接中断、权限突然失效
- 不适合抛异常:用户输入邮箱格式错误、订单状态非法(应走状态机校验)
- 过度使用自定义异常会导致 try/catch 泛滥,掩盖真正需要关注的故障点
GetObjectData 的配对实现——缺一不可,否则在 .NET Core+ 跨进程场景下反序列化会失败,且错误提示极不友好。







