如何让异常对象携带额外上下文信息(cause vs context)

异常需区分cause(根本原因,用于异常链和栈追踪)与context(执行上下文,用于诊断日志);cause须通过构造函数显式设置,context应作为只读字段存于自定义异常中并结构化输出。

异常对象携带额外上下文信息,关键在于区分 cause(根本原因)context(执行上下文):前者用于构建异常链、支持栈追踪回溯;后者用于辅助诊断、日志记录或调试,但不参与异常传播逻辑。

用 cause 表达嵌套异常(真正的“因为”)

当一个异常由另一个异常引发时,应通过构造函数的 cause 参数(如 Java 的 Throwable(Throwable cause),Python 的 raise NewError() from original_exc)显式关联。这会让运行时保留原始异常的完整栈,调用 getCause()__cause__ 可获取它,打印异常时也会自动显示 “Caused by”。

  • Java 示例:throw new IllegalArgumentException("Invalid user ID", originalException);
  • Python 示例:raise ValueError("Processing failed") from exc
  • 避免手动拼接消息代替 cause——那样会丢失栈、无法被工具识别、破坏异常分类逻辑

用 context 字段或属性附加诊断信息(当前“在哪、谁、什么状态”)

cause 解决“为什么发生”,context 解决“当时发生了什么”。推荐在自定义异常类中添加只读字段(如 userId, requestId, inputData),初始化时传入并存为实例属性。

  • Java:定义 public final String requestId;,在构造器中赋值
  • Python:在 __init__ 中设 self.request_id = request_id
  • 重写 toString()__str__ 时包含这些字段,方便日志直接输出上下文
  • 不建议把 context 塞进 message 字符串——难解析、易重复、干扰错误模式匹配

避免混淆 cause 与 context 的常见错误

把本该是 context 的信息(如用户ID、时间戳)当作 cause 抛出,会导致异常链失真;反过来,把底层 I/O 异常仅作为 context 记录而不设为 cause,则丢失关键根因线索。

  • ❌ 错误:捕获 IOException 后创建新异常时,用 message += "user=123" 而不设 cause
  • ✅ 正确:以 IOException 为 cause 构造新异常,并单独保存 userId 字段
  • ❌ 错误:为记录请求 ID,包装异常时用 new RuntimeException(original, requestId)——把 context 当 cause
  • ✅ 正确:自定义异常类同时持有 cause(父异常)和 requestId(context 字段)

配合日志与监控高效利用 context

异常对象中的 context 字段本身不自动出现在日志里,需主动提取。在全局异常处理器或日志 AOP 中,检查异常是否含自定义 context 属性,并将其作为结构化字段(如 MDC、log attributes)注入日志事件。

  • 例如:Logback 的 MDC 可在捕获异常后 MDC.put("requestId", e.getRequestId())

  • 监控系统(如 Sentry)支持 attach extra data,应传入异常的 context 属性而非拼接字符串
  • 确保 context 字段是不可变、线程安全的值(如 String、Long),避免运行时被意外修改