正确做法是修改原异常的args后直接raise exc,不使用from;Go用%w保留底层错误类型;Java优先用带cause的构造器;JS用error.cause元数据。
raise ... from 会改变异常链,但不改类型想保留原始异常类型(比如仍是 ValueError),又想附加上下文信息,不能靠修改 args 或新建同名异常——那样容易丢失 traceback 或触发误判。正确做法是用隐式异常链:raise 后不跟 from,而是先修改原异常的 args,再重新抛出。
exc.args = (exc.args[0] + " [context: db timeout]",),然后 raise exc
exc.args 是空元组或非字符串首项,先转成字符串再拼接,否则可能报 TypeError
__cause__ 或 __context__,所以 except ValueError 仍能精准捕获fmt.Errorf 包装时保留底层错误类型Go 没有“异常类型继承”概念,但标准库鼓励用 errors.Is 和 errors.As 判断底层错误。所以包装时要用 %w 动词,而不是 %s:
err := doSomething()
if err != nil {
return fmt.Errorf("failed to process user %d: %w", userID, err)
}
%w 会让新错误实现 Unwrap() 方法,使 errors.As(err, &target) 能命中原始错误类型%s,原始错误被转成字符串,errors.As 就失效了%w 只能有一个,嵌套过深会影响性能,一般不超过两层addSuppressed 不适用,该用 initCause 或构造器传参addSuppressed 是为 try-with-resources 的多重异常设计的,它不改变主异常类型,但也不附加到消息里;真要加文本报错信息,得走初始化路径:
Throwable cause 的构造器(如 IOException(String, Throwable)),优先用它,既保类型又留链initCause(),但必须在构造后立即调用,且只能调一次,否则抛 IllegalStateException
getMessage() + " extra" 后塞进新异常——新异常类型变了,下游 instanceof 就断了error.cause 是补充字段,不影响 instanceof
ES2025 起支持 cause 选项,它纯粹是元数据,完全不影响类型判断:
try {
riskyOperation();
} catch (err) {
throw new TypeError("Validation failed for input", { cause: err });
}
TypeError 实例,err instanceof TypeError 为 true
err.cause 可访问原始错误,但需运行时检查是否存在(旧环境无此属性)err.original = originalErr,但别覆盖内置属性名最易被忽略的是:所有语言里“附加信息”都不该破坏原始错误的可序列化结构——比如 Python 的 __dict__ 扩展、Go 的自定义字段、Java 的非 transient 字段,都可能让日志系统或 RPC 框架丢掉关键上下文。