资讯详情

Effect `Data.Error` / `Data.TaggedError` 的 `toString` 修复:对齐原生 `Error` 输出格式(1940)

📅 2026/9/15 16:51:20 | 华诺云谱 👁 阅读
Effect `Data.Error` / `Data.TaggedError` 的 `toString` 修复:对齐原生 `Error` 输出格式(1940)
EffectData.Error/Data.TaggedError的toString修复对齐原生Error输出格式#1940【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code导读本篇文章围绕 effect-smol 仓库中一条真实的 changeset 修复记录展开深入解析 Effect 类型化错误体系Data.Error、Data.TaggedError在序列化输出上的一个关键缺陷与修复方案自定义错误在打印时输出E({message:my message})这类对象字面量而不是原生Error的标准格式E: my message。读完本文你将理解 Effect 可失败错误的底层实现原理、toString与原型链的关系、.extend()之后原型属性丢失的成因以及如何通过测试验证这类行为。变更记录定位与背景本次分析的核心证据是仓库中的 changeset 记录 fix-1940.md原路径.repos/effect-smol/.changeset/fix-1940.md实际位于pre/目录下其完整内容如下--- effect: patch --- Fix ErrorClass and TaggedErrorClass toString to match native Error output format (e.g. E: my message instead of E({message:my message})), closes #1940. Also fix prototype properties (e.g. name) being lost after .extend().这是一次作用于effect包的patch级别修复包含两个要点toString输出格式对齐原生Error修复前Data.Error/Data.TaggedError实例的字符串化结果形如E({message:my message})一个对象字面量风格的序列化输出修复后与原生Error一致输出E: my message这种错误名: message的格式。.extend()后原型属性丢失例如name这类定义在原型上的属性在通过.extend()派生新错误类之后不再保留。在 CHANGELOG.md 中可以看到同一变更的完整收录对应 PR #1942确认这是随effect包常规发版流程进入正式版本的修复。为什么 Effect 需要自定义错误类在 Effect 的编程模型中失败是程序的一等公民。为了让错误既能携带结构化数据又能被类型安全地捕获Effect 在Data模块中提供了两个核心错误构造器Data.ErrorFields创建一个继承自globalThis.Error的类型化错误类构造时传入的字段会直接挂载到实例上Data.TaggedError(Tag)Fields在Data.Error基础上额外注入_tag字段使错误具备可辨识的标签配合Effect.catchTag实现按标签分类捕获。它们在源码中的定义位于 internal/core.ts整体结构如下// 可被 Effect 运行时产出为失败结果的基础错误基类 class YieldableError extends globalThis.Error {} // Data.Error export const Error: newA extends Recordstring, any {}(...) (function() { return class Base extends YieldableError { constructor(args) { super(args?.message, args?.cause ? { cause: args.cause } : undefined) if (args) { InternalRecord.assignProperties(this, args) // ... } } override toJSON() { return { ...(this as any)[plainArgsSymbol], ...this } } } })() // Data.TaggedError export const TaggedError Tag extends string(tag: Tag) { class Base extends Error{} { readonly _tag tag } ;(Base.prototype as any).name tag return Base as any }从这段实现可以看出三个关键设计构造函数将message、cause透传给原生Error其余字段通过assignProperties平铺到实例上toJSON()返回「原始构造参数 实例字段」的合并结果保证结构化数据在序列化时不丢失TaggedError通过(Base.prototype as any).name tag把标签写进原型上的name属性——这正是本次 changeset 第二处修复所涉及的属性。Data模块的公开入口Data.ts将TaggedClass、TaggedError等构造器统一导出并在注释中与TaggedError互相链接方便用户查阅两者的区别。Bug #1940toString为何输出E({message:my message})要理解修复前的异常输出需要回到YieldableError的构造过程。YieldableError的原型并不是一个干净的Error子类而是通过makePrimitiveProto工厂函数注入了 Effect 自定义的原型方法internal/core.tsclass YieldableError extends globalThis.Error {} const proto makePrimitiveProto({ op: YieldableError, ... }) delete (proto as any).toString // ← 关键修复点 Object.assign(YieldableError.prototype, proto)makePrimitiveProto生成的proto中包含了 Effect 自定义的toString实现在 internal/core.ts 与 internal/core.ts 等处可见此类自定义toString的身影ReasonBase及其子类Fail也各自override toString()以实现 Cause 的格式化输出。当这个带自定义toString的proto被Object.assign到YieldableError.prototype上时它遮蔽了继承自globalThis.Error.prototype的原生toString。于是Data.Error/Data.TaggedError实例在字符串化时不再走原生逻辑而是落入 Effect 自定义的序列化路径把实例当作普通对象打印产生了E({message:my message})这样的输出。这在以下场景会明显暴露问题直接拼接字符串 myErrorconsole.log展开对象之前的隐式字符串转换日志系统、错误上报工具对Error的标准文本提取依赖error.toString()或String(error)的第三方库。修复方式本身非常精炼——delete (proto as any).toString。删掉原型上的自定义toString后原型链上的查找会越过YieldableError.prototype最终命中Error.prototype.toString其行为是输出name: messagename取原型上的类名或标签message取实例的message字段于是得到与原生Error完全一致的E: my message格式。这是一次典型的移除遮蔽、回落到原生实现的原型链修复。第二处修复.extend()之后原型属性丢失changeset 同时声明修复了.extend()派生后原型属性如name丢失的问题。之所以name会成为重灾区正是因为TaggedError把标签写入的是Base.prototype.nameinternal/core.ts——它是原型属性而不是实例自身属性。当用户通过.extend()基于某个错误类派生新的子类时如果派生过程重新生成/替换了原型或构造链没有正确串联super那么定义在父类原型上的name就会在派生类实例上失联表现为实例的name变成默认的Error甚至undefined进而导致toString()name: message格式的第一段输出错误。从仓库测试可以确认name是TaggedError的一项明确契约在 Data.test.ts 中有专门用例sets .name to the tag断言new NotFound({ resource: x }).name NotFound。因此.extend()破坏name会被视为行为回归并在本次 patch 中一并修复。测试验证错误类的完整行为契约Data.test.ts 中围绕Error/TaggedError的用例实际上勾勒出了本次修复所维护的行为契约全集契约点测试位置断言内容必须是真正的原生错误describe(Error)首条用例e instanceof globalThis.Error为真字段正确挂载assigns fieldse.code 404、e.message not found拥有调用栈has a stack tracetypeof e.stack string且非空无字段类可无参构造allows errors without fields...new Empty()可用且是Error实例可作为失败产出can be yielded in Effect.gen to failyield* new MyError(...)后exit._tag Failure序列化包含字段toJSON includes fieldsjson.code、json.message均存在标签字段sets _tage._tag NotFound可按标签捕获can be caught with Effect.catchTagcatchTag(NotFound, ...)命中并返回e.resource原型name契约sets .name to the tage.name NotFound不同标签不等价different tags produce non-equal errorsEqual.equals(a, b) false其中与本次 changeset 直接相关的就是sets .name to the tag原型属性契约以及instanceof globalThis.Error/toJSON等围绕错误本质的断言。修复后String(myError)应从E({message:my message})变为E: my message同时.extend()派生类实例的name依旧指向标签值。实际使用定义与输出效果结合上述实现一个典型的Data.TaggedError定义与使用如下可参照 Data.test.ts 的写法import { Data, Effect } from effect class NotFound extends Data.TaggedError(NotFound){ readonly resource: string } {} const program Effect.gen(function*() { throw new NotFound({ resource: /users }) }).pipe( Effect.catchTag(NotFound, (e) Effect.succeed(caught: ${e.resource})) ) const result await Effect.runPromise(program) console.log(result) // caught: /users在本次修复之前new NotFound({ resource: /users }).toString()会输出对象字面量风格的结果修复之后则输出NotFound: /users与new Error(/users)的Error: /users输出风格完全对齐便于日志聚合、错误追踪系统统一解析。升级与兼容性提示本次变更等级为patch属于行为修正而非破坏性变更toString()的文本格式从自定义对象风格调整为原生风格对依赖JSON.stringify走toJSON不受影响的代码无影响但依赖旧toString文本格式做字符串匹配的代码需要留意输出变化。若你正在以错误文本格式做告警关键词匹配例如把({message:作为识别特征建议升级后改用name或_tag字段进行判断它们更为稳定。修复同时保证了.extend()派生场景下原型属性尤其是name的保留涉及错误类继承体系的代码可以放心升级。小结这条小小的 changeset 背后折射出 Effect 错误系统设计的两个核心原则类型化错误首先必须是真正的Error继承自globalThis.Error、拥有stack、支持cause同时又要具备 Effect 生态特有的可标签化、可结构序列化能力。toString的对齐修复通过delete proto.toString让原生Error的输出语义回归而name在.extend()后的保留则守住了TaggedError的标签契约——两者共同保证了 Effect 错误在自家运行时与外部 JS 生态两个世界里都表现一致。相关源码与测试可分别在 internal/core.ts 与 Data.test.ts 中继续深入。【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。