Error code property S2
中文标题:错误码属性
- 阶段: Stage 2
- 状态: 进行中
- ECMAScript 版本: —
- 同步时间: 2026年8月26日
- English original · 官方仓库
该提案标准化了 ECMAScript 错误对象上的 code 属性,允许开发者通过稳定的、机器可读的标识符以编程方式识别错误条件,而不是依赖消息字符串或 instanceof。它扩展了 Error 构造函数的选项包以接受 code 属性,适用于所有原生错误类型,该属性默认不存在且类型不限。js、Deno、Bun)和库(axios、Stripe 等)对 error.code 的广泛采用,同时明确将代码值和规范定义的代码排除在范围之外。
以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。
提案:ECMAScript 错误对象的 code 属性
状态
Stage 2
提案发起人
James M Snell
审阅者
Jordan Harband
使用场景
标准化的 code 属性将能够实现:
-
精确的程序化错误处理。 应用程序代码可以根据特定的错误条件进行分支处理,而无需依赖消息字符串:
-
跨版本稳定的错误契约。 错误码是一种机器可读的标识符,可以被文档化、版本化并被依赖——这与消息不同,消息是实现定义的散文。
-
跨上下文错误识别。 与
instanceof不同,字符串类型的.code属性能够经受结构化克隆、序列化和跨上下文传输。 -
改进的错误报告和遥测。 在监控系统(Sentry、Datadog 等)中按
.code聚合错误比按消息聚合更可靠、更有用,因为消息在不同引擎和版本之间会有所差异。 -
错误翻译和国际化。 通过稳定的代码,错误消息可以被本地化或映射到面向用户的文本,而不会丢失机器可读性。
-
生态对齐。 提供一个生态已经使用的标准属性将减少碎片化,并为库作者提供一个受支持的模式。
-
更好的开发者体验。 文档和工具可以引用错误码。开发人员可以搜索
ERR_INVALID_ARG_TYPE并找到确定的文档,而搜索消息字符串则不可靠。
超出范围
在向 TC39 提交 Stage 1 时,有人提出委员会是否应为 TC39 定义的错误码预留一部分 code 命名空间,以及委员会是否应为规范定义的抛出场景定义自己的 code 值。
这带来了一些挑战,原因如下:
- 很难进行改造。规范定义了大量的抛出条件。对于实现来说,追溯更新错误处理逻辑以添加错误码并将其传播到现有逻辑中,将是一项重大工程。这样做的好处可能微不足道,不值得付出努力。
- 许多实现层面的优化导致通常不清楚在特定情况下应用哪种代码。例如,抛出可能源于多个路径使用的工具函数,或者规范定义的单个“可观察”抛出实际上可能源于代码中的多个位置,甚至可能随时间变化,这使得管理更加困难。
code命名空间应该是什么很难确定。虽然生态已经采用了代码的使用,但它们尚未就通用命名或命名空间约定达成一致,我们可能强加的任何内容要么与生态冲突,要么不合理地约束生态。
就本提案而言,我们考虑将规范定义的 code 的定义以及是否为规范定义的错误分配这些 code 均视为超出范围;这最好在单独的后续提案中解决(如果有的话)。
先前实践调查
JavaScript 生态已经压倒性地、独立地采用了 error.code 作为解决方案。本节调查了现状。
JavaScript 运行时
Node.js
最成熟的实现。Node.js 自 v8.0(2017 年)起就在错误上使用了字符串类型的 .code 属性,并提供了数百个有文档记录的代码。
- 类型:
string(实例属性) - 约定: Node.js 错误使用
ERR_*前缀(例如,ERR_INVALID_ARG_TYPE、ERR_BUFFER_OUT_OF_BOUNDS、ERR_HTTP2_INVALID_HEADER_VALUE)。系统错误使用 POSIX 代码(ENOENT、EACCES、ECONNREFUSED)。 - 范围: 200 多个按子系统组织的文档化
ERR_*代码,以及 POSIX 和 OpenSSL 代码。 - 架构: 内部的
NodeErrorAbstraction类扩展了原生类型(TypeError、RangeError等),并在构造函数中设置.code。 - 设计选择: Node.js 特意选择了字符串而非数字,以便错误处理代码无需查找幻数即可自文档化。
- 稳定性承诺: 错误码是稳定 API 的一部分。移除或更改代码的语义属于破坏性更改。
Deno
Deno 在其 node:* 兼容层中完全镜像了 Node.js 的 .code:
- Node 兼容: 使用与 Node.js 相同的
ERR_*字符串代码。 - 原生 API: 使用类层次结构(
Deno.errors.NotFound、Deno.errors.PermissionDenied),依赖instanceof检查。POSIX errno 字符串(例如ENOENT、ECONNREFUSED)通过底层操作系统错误作为.code附加到 I/O 错误上,但这并未作为公共 API 记录在案。
Deno 在其兼容层中完全采用 Node.js 错误码的事实证明了该模式对于互操作性至关重要。Deno 原生选择的基于 instanceof 的错误类突出了该方法的局限性——它无法经受结构化克隆或跨上下文传输,并且需要导入特定的错误构造函数。
Deno 也未将 DOMException 实现为可结构化克隆的类型,因此克隆时会丢失所有类型信息,包括修改后的 .name 属性。
Bun
Bun 对所有 node:* 模块错误镜像了 Node.js 的 .code,并将相同的约定扩展到了自己的原生 API:
- 使用相同的
ERR_*约定和 POSIX 代码处理系统错误。 - Bun 原生 API 使用原始的
ERR_*代码:例如,内置 Postgres 客户端使用ERR_POSTGRES_*,Redis 使用ERR_REDIS_*,S3 使用ERR_S3_*。
Bun 决定为自己的非 Node API 采用 ERR_* 代码——而不是发明单独的错误识别方案——这有力地证明了 .code 是 JavaScript 错误的自然扩展点。
Cloudflare Workers
Cloudflare Workers 也镜像了 Node.js 错误码以处理 node:* 模块错误
- 使用相同的
ERR_*约定以保证兼容性。 - 原生 API 抛出标准的
Error,不带有自定义代码。
Web 平台
DOMException
DOMException 既有传统的数值型 .code,也有现代的字符串型 .name:
.code(number):已废弃。 传统的常量,如NOT_FOUND_ERR = 8、NOT_SUPPORTED_ERR = 9。对于 ~2012 年之后添加的所有较新错误类型,返回0。.name(string):现代标识符。PascalCase:"NotFoundError"、"AbortError"、"DataCloneError"。
Web 平台明确弃用数字型 .code 而采用字符串型 .name 是一个明确的信号:基于字符串的错误识别是正确的路径。
MediaError
- 类型:
number - 代码:
MEDIA_ERR_ABORTED = 1、MEDIA_ERR_NETWORK = 2、MEDIA_ERR_DECODE = 3、MEDIA_ERR_SRC_NOT_SUPPORTED = 4
GeolocationPositionError
- 类型:
number - 代码:
PERMISSION_DENIED = 1、POSITION_UNAVAILABLE = 2、TIMEOUT = 3
RTCError
- 继承自 DOMException(
.code+.name),但增加了字符串类型的.errorDetail属性("sdp-syntax-error"、"dtls-failure"等)用于领域特定识别。
趋势
较旧的 Web API 使用数字代码。较新的使用字符串。平台已转向基于字符串的识别。
流行库
以下主要库独立采用了 error.code:
字符串代码占主导地位。 数字代码仅出现在上游协议定义它们的地方(gRPC、MongoDB)。
其他语言
大多数语言使用类型层次结构作为主要机制,并提供可选的字符串/数字代码与外部系统(操作系统、数据库、协议)互操作。JavaScript 缺乏适用于此目的的实际类型层次结构(内置子类型有限,instanceof 存在跨上下文问题),因此基于属性的方法更合适。
为什么不直接使用 error.name?
error.name 已经存在,并默认为构造函数名称("TypeError"、"RangeError" 等)。但是:
.name是粗粒度的——所有 TypeError 共享相同的.name。.code在错误类型内提供细粒度的识别。- 它们是互补的:
.name表示 哪种 错误;.code表示 哪个具体的 错误条件。 - 重写
.name来编码特定条件会混淆两个关注点,并破坏基于instanceof的预期。
DOMException 是一个警示性的例子,说明了当 .name 被重新用于承载特定错误身份时会发生什么。每个 DOMException 实例的 .name 都设置为特定的错误字符串,如 "NotFoundError" 或 "AbortError",而不是 "DOMException"。这意味着:
instanceof和.name不一致。err instanceof DOMException为true,但err.name是"AbortError",而不是"DOMException"。这破坏了.name反映构造函数/类型的基本预期。error.name承担了分发逻辑。 代码必须根据.name进行分支以区分 DOMException 子类型,这使得.name事实上的错误码,同时名义上仍是“类型名称”。这正是.code应该承担的角色。- 它排除了子类化。 由于
.name已经承载了特定的错误身份,DOMException 子类没有空间拥有自己的.name而不失去错误身份,也没有空间让.name反映实际的类层次结构。 - 堆栈跟踪和日志记录具有误导性。 记录的 DOMException 显示其
.name为"AbortError",看起来像一个不存在于类层次结构中的独立错误类。开发人员搜索AbortError构造函数却找不到任何东西(或者发现AbortError只是带有特定.name的DOMException)。
如果 .code 作为标准属性存在,DOMException 本可以使用 { code: "AbortError" },同时将 .name 保持为 "DOMException",从而保持 .name、instanceof 和类层次结构之间的自然关系。
拟议设计
API
扩展 Error 构造函数的选项包(ES2022 为 cause 引入的)以接受 code 属性。这适用于 Error、所有 NativeError 类型(TypeError、RangeError 等)、AggregateError 和 SuppressedError:
.code 属性将是:
- 定义在实例上,而不是
Error.prototype上 - 类型: 任意值(不限于字符串,与
cause一致) - 默认值: 未提供时不存在该属性(
'code' in err为false),与cause一致 - 可枚举:
false(与cause和message一致) - 可写:
true(与其他 Error 属性一致) - 可配置:
true
为什么类型不限制为 string?
虽然字符串是生态中的主导约定(Node.js、axios、Firebase、Stripe 等),但规范不应限制类型。几个主要库使用数字代码(gRPC、MongoDB、TypeScript 诊断),而 cause 已经确立了接受任何值且不进行类型限制的先例。
如前文调查所述,生态强烈倾向于使用字符串——自文档化、无需查找表、无碰撞风险——但这最好作为约定而非语言级约束。
为什么默认不存在,而非强制性
- 向后兼容:现有的
new Error("msg")调用应保持工作不变。 - 并非所有错误都有有意义的代码(例如,临时性的
throw new Error("bug"))。 - 遵循
cause的先例,未提供时同样不存在。
与现有提案的关系
Error.cause(ES2022):在Error构造函数上建立了选项包模式。本提案将同一选项包扩展为包含code。- 错误堆栈(Stage 1):专注于标准化
error.stack。正交但互补——堆栈可以包含代码。相关提案将利用选项包处理其他元数据。 Error.isError(Stage 2):跨上下文错误识别。互补——.code在已确认的错误内提供细粒度识别。
常见问题解答
这难道不会鼓励字符串类型化编程吗?
不会比 error.message 和 error.name 在实践中已经做到的更多。关键区别在于,.code 被明确设计为机器可读的标识符,理想情况下具有稳定性保证,而 .message 是人类可读的散文。.code 在语义上等同于标记联合中的判别式——它只是使用字符串(通常)而不是类型标签。
难道每个库都会发明自己的代码,导致混乱吗?
它们已经这样做了——这就是当前的情况。标准化 属性 并不要求标准化 值。它为代码提供了一个受支持的位置(而不是像 .errno、.errorCode、.errCode 等临时属性),并使得工具能够围绕单一约定构建。
本提案是否定义了新的错误分类法?
否。本提案不规定任何具体的代码或分类法。它只是提供一个标准属性,供库和应用程序选择使用。生态可以有机发展,流行的代码将作为事实标准出现(例如 Node.js 中的 ERR_INVALID_ARG_TYPE)。
为什么不鼓励使用基于 Symbol 的代码而不是字符串?
Symbol 可以防止碰撞,但会牺牲字符串代码的关键优势:序列化、日志记录、遥测聚合、人类可读性和跨上下文传输。Node.js 生态已经证明,带前缀约定(ERR_*)的字符串代码是实用且抗碰撞的。
但由于代码值可以是任何类型,库和应用程序如果愿意可以使用 Symbol——本提案并不排除这一点。
难道不能用错误子类解决这个问题吗?
理论上可以——类层次结构可以编码任何错误分类法。但实践中:
instanceof在上下文之间会失效。- 内置错误层次结构太浅(只有大约 7 种类型)。
- 为每个错误条件创建深层类层次结构在工程上很繁重。
- 错误子类不能在
catch中进行模式匹配(标准 JS 中没有catch (e if ...))。 - 生态已经用行动投票:在实例上使用
.code。 - 错误的结构化克隆和跨上下文传输很常见(例如
postMessage),而类无法经受这些。
我们在 Deno 的 Deno.errors.* 类命名空间中看到了这些限制,这些类无法在克隆边界之间可靠识别,并且 structuredClone 等的运行时支持不一。