For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/stage/2/proposal-error-code-property.md.
  • 简体中文
  • Error code property S2

    中文标题:错误码属性

    提案概览
    提案速览

    该提案标准化了 ECMAScript 错误对象上的 code 属性,允许开发者通过稳定的、机器可读的标识符以编程方式识别错误条件,而不是依赖消息字符串或 instanceof。它扩展了 Error 构造函数的选项包以接受 code 属性,适用于所有原生错误类型,该属性默认不存在且类型不限。js、Deno、Bun)和库(axios、Stripe 等)对 error.code 的广泛采用,同时明确将代码值和规范定义的代码排除在范围之外。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    提案:ECMAScript 错误对象的 code 属性

    状态

    Stage 2

    提案发起人

    James M Snell

    审阅者

    Jordan Harband

    使用场景

    标准化的 code 属性将能够实现:

    1. 精确的程序化错误处理。 应用程序代码可以根据特定的错误条件进行分支处理,而无需依赖消息字符串:

      try {
        await db.connect();
      } catch (e) {
        if (e.code === 'ERR_CONNECTION_REFUSED') {
          // 带退避重试
        }
      }
    2. 跨版本稳定的错误契约。 错误码是一种机器可读的标识符,可以被文档化、版本化并被依赖——这与消息不同,消息是实现定义的散文。

    3. 跨上下文错误识别。instanceof 不同,字符串类型的 .code 属性能够经受结构化克隆、序列化和跨上下文传输。

    4. 改进的错误报告和遥测。 在监控系统(Sentry、Datadog 等)中按 .code 聚合错误比按消息聚合更可靠、更有用,因为消息在不同引擎和版本之间会有所差异。

    5. 错误翻译和国际化。 通过稳定的代码,错误消息可以被本地化或映射到面向用户的文本,而不会丢失机器可读性。

    6. 生态对齐。 提供一个生态已经使用的标准属性将减少碎片化,并为库作者提供一个受支持的模式。

    7. 更好的开发者体验。 文档和工具可以引用错误码。开发人员可以搜索 ERR_INVALID_ARG_TYPE 并找到确定的文档,而搜索消息字符串则不可靠。

    超出范围

    在向 TC39 提交 Stage 1 时,有人提出委员会是否应为 TC39 定义的错误码预留一部分 code 命名空间,以及委员会是否应为规范定义的抛出场景定义自己的 code 值。

    这带来了一些挑战,原因如下:

    1. 很难进行改造。规范定义了大量的抛出条件。对于实现来说,追溯更新错误处理逻辑以添加错误码并将其传播到现有逻辑中,将是一项重大工程。这样做的好处可能微不足道,不值得付出努力。
    2. 许多实现层面的优化导致通常不清楚在特定情况下应用哪种代码。例如,抛出可能源于多个路径使用的工具函数,或者规范定义的单个“可观察”抛出实际上可能源于代码中的多个位置,甚至可能随时间变化,这使得管理更加困难。
    3. code 命名空间应该是什么很难确定。虽然生态已经采用了代码的使用,但它们尚未就通用命名或命名空间约定达成一致,我们可能强加的任何内容要么与生态冲突,要么不合理地约束生态。

    就本提案而言,我们考虑将规范定义的 code 的定义以及是否为规范定义的错误分配这些 code 均视为超出范围;这最好在单独的后续提案中解决(如果有的话)。

    先前实践调查

    JavaScript 生态已经压倒性地、独立地采用了 error.code 作为解决方案。本节调查了现状。

    JavaScript 运行时

    Node.js

    最成熟的实现。Node.js 自 v8.0(2017 年)起就在错误上使用了字符串类型的 .code 属性,并提供了数百个有文档记录的代码。

    • 类型: string(实例属性)
    • 约定: Node.js 错误使用 ERR_* 前缀(例如,ERR_INVALID_ARG_TYPEERR_BUFFER_OUT_OF_BOUNDSERR_HTTP2_INVALID_HEADER_VALUE)。系统错误使用 POSIX 代码(ENOENTEACCESECONNREFUSED)。
    • 范围: 200 多个按子系统组织的文档化 ERR_* 代码,以及 POSIX 和 OpenSSL 代码。
    • 架构: 内部的 NodeErrorAbstraction 类扩展了原生类型(TypeErrorRangeError 等),并在构造函数中设置 .code
    • 设计选择: Node.js 特意选择了字符串而非数字,以便错误处理代码无需查找幻数即可自文档化。
    • 稳定性承诺: 错误码是稳定 API 的一部分。移除或更改代码的语义属于破坏性更改。
    // 带有 .code 的 Node.js 错误
    const fs = require('fs');
    try {
      fs.readFileSync('/nonexistent');
    } catch (e) {
      e.code;    // 'ENOENT'
      e.message; // "ENOENT: no such file or directory, open '/nonexistent'"
    }
    Deno

    Deno 在其 node:* 兼容层中完全镜像了 Node.js 的 .code

    • Node 兼容: 使用与 Node.js 相同的 ERR_* 字符串代码。
    • 原生 API: 使用类层次结构(Deno.errors.NotFoundDeno.errors.PermissionDenied),依赖 instanceof 检查。POSIX errno 字符串(例如 ENOENTECONNREFUSED)通过底层操作系统错误作为 .code 附加到 I/O 错误上,但这并未作为公共 API 记录在案。

    Deno 在其兼容层中完全采用 Node.js 错误码的事实证明了该模式对于互操作性至关重要。Deno 原生选择的基于 instanceof 的错误类突出了该方法的局限性——它无法经受结构化克隆或跨上下文传输,并且需要导入特定的错误构造函数。

    // 在 Deno 中运行
    let e = new Deno.errors.PermissionDenied()
    console.log(e instanceof Deno.errors.PermissionDenied) // true
    console.log(e.name) // "PermissionDenied"
    
    // 错误的细节无法经受结构化克隆
    let clone = structuredClone(e)
    console.log(clone instanceof Deno.errors.PermissionDenied) // false
    console.log(clone.name) // "Error"
    
    // 也无法经受 postMessage
    const { port1, port2 } = new MessageChannel();
    port1.postMessage(e);
    port2.onmessage = (event) => {
      const received = event.data;
      console.log(received instanceof Deno.errors.PermissionDenied) // false
      console.log(received.name) // "Error"
    };

    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

    • .codenumber):已废弃。 传统的常量,如 NOT_FOUND_ERR = 8NOT_SUPPORTED_ERR = 9。对于 ~2012 年之后添加的所有较新错误类型,返回 0
    • .namestring):现代标识符。PascalCase:"NotFoundError""AbortError""DataCloneError"

    Web 平台明确弃用数字型 .code 而采用字符串型 .name 是一个明确的信号:基于字符串的错误识别是正确的路径。

    MediaError
    • 类型: number
    • 代码: MEDIA_ERR_ABORTED = 1MEDIA_ERR_NETWORK = 2MEDIA_ERR_DECODE = 3MEDIA_ERR_SRC_NOT_SUPPORTED = 4
    GeolocationPositionError
    • 类型: number
    • 代码: PERMISSION_DENIED = 1POSITION_UNAVAILABLE = 2TIMEOUT = 3
    RTCError
    • 继承自 DOMException(.code + .name),但增加了字符串类型的 .errorDetail 属性("sdp-syntax-error""dtls-failure" 等)用于领域特定识别。
    趋势

    较旧的 Web API 使用数字代码。较新的使用字符串。平台已转向基于字符串的识别。

    流行库

    以下主要库独立采用了 error.code

    .code 类型约定
    axiosstringERR_NETWORKERR_CANCELEDETIMEDOUT
    Firebasestring"auth/user-not-found""storage/not-found"
    Stripestring"card_declined""rate_limit"
    Prismastring"P2002""P2025""P1001"
    pg (Postgres)stringSQLSTATE 代码:"23505""42P01"
    mysql2string"ER_DUP_ENTRY""ER_ACCESS_DENIED_ERROR"
    MongoDB/mongoosenumberMongoDB 服务器代码:11000
    @grpc/grpc-jsnumbergRPC 状态代码:5 (NOT_FOUND)
    AWS SDK v3string"AccessDenied""NoSuchBucket"
    Zodstring"invalid_type""too_small"

    字符串代码占主导地位。 数字代码仅出现在上游协议定义它们的地方(gRPC、MongoDB)。

    其他语言

    语言机制类型
    Python异常类层次结构 + OSError.errnoclass + int
    Ruststd::io::ErrorKind 枚举 + .raw_os_error()enum + i32
    Go哨兵值(os.ErrNotExist)+ errors.Is()value identity
    Java异常层次结构 + SQLException.getSQLState()class + String
    C#/.NET异常层次结构 + Exception.HResultclass + int

    大多数语言使用类型层次结构作为主要机制,并提供可选的字符串/数字代码与外部系统(操作系统、数据库、协议)互操作。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 DOMExceptiontrue,但 err.name"AbortError",而不是 "DOMException"。这破坏了 .name 反映构造函数/类型的基本预期。
    • error.name 承担了分发逻辑。 代码必须根据 .name 进行分支以区分 DOMException 子类型,这使得 .name 事实上的错误码,同时名义上仍是“类型名称”。这正是 .code 应该承担的角色。
    • 它排除了子类化。 由于 .name 已经承载了特定的错误身份,DOMException 子类没有空间拥有自己的 .name 而不失去错误身份,也没有空间让 .name 反映实际的类层次结构。
    • 堆栈跟踪和日志记录具有误导性。 记录的 DOMException 显示其 .name"AbortError",看起来像一个不存在于类层次结构中的独立错误类。开发人员搜索 AbortError 构造函数却找不到任何东西(或者发现 AbortError 只是带有特定 .nameDOMException)。

    如果 .code 作为标准属性存在,DOMException 本可以使用 { code: "AbortError" },同时将 .name 保持为 "DOMException",从而保持 .nameinstanceof 和类层次结构之间的自然关系。

    拟议设计

    API

    扩展 Error 构造函数的选项包(ES2022 为 cause 引入的)以接受 code 属性。这适用于 Error、所有 NativeError 类型(TypeErrorRangeError 等)、AggregateErrorSuppressedError

    new Error("something went wrong", { code: "ERR_SOMETHING" })
    new TypeError("expected string", { code: "ERR_INVALID_ARG_TYPE", cause: original })

    .code 属性将是:

    • 定义在实例上,而不是 Error.prototype
    • 类型: 任意值(不限于字符串,与 cause 一致)
    • 默认值: 未提供时不存在该属性('code' in errfalse),与 cause 一致
    • 可枚举: false(与 causemessage 一致)
    • 可写: true(与其他 Error 属性一致)
    • 可配置: true

    为什么类型不限制为 string

    虽然字符串是生态中的主导约定(Node.js、axios、Firebase、Stripe 等),但规范不应限制类型。几个主要库使用数字代码(gRPC、MongoDB、TypeScript 诊断),而 cause 已经确立了接受任何值且不进行类型限制的先例。

    如前文调查所述,生态强烈倾向于使用字符串——自文档化、无需查找表、无碰撞风险——但这最好作为约定而非语言级约束。

    为什么默认不存在,而非强制性

    1. 向后兼容:现有的 new Error("msg") 调用应保持工作不变。
    2. 并非所有错误都有有意义的代码(例如,临时性的 throw new Error("bug"))。
    3. 遵循 cause 的先例,未提供时同样不存在。

    与现有提案的关系

    • Error.cause(ES2022):在 Error 构造函数上建立了选项包模式。本提案将同一选项包扩展为包含 code
    • 错误堆栈(Stage 1):专注于标准化 error.stack。正交但互补——堆栈可以包含代码。相关提案将利用选项包处理其他元数据。
    • Error.isError(Stage 2):跨上下文错误识别。互补——.code 在已确认的错误内提供细粒度识别。

    常见问题解答

    这难道不会鼓励字符串类型化编程吗?

    不会比 error.messageerror.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 等的运行时支持不一。