尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Relay 19 的 @catch 指令实战指南:将字段级错误处理从“隐式 null“升级为“显式数据“

发布时间:2026/9/24 14:36:37

资讯中心
01
ARTICLE

Relay 19 的 @catch 指令实战指南:将字段级错误处理从“隐式 null“升级为“显式数据“

Relay 19 的 @catch 指令实战指南:将字段级错误处理从“隐式 null“升级为“显式数据“
前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载本篇技术指南围绕 Relay 的catch指令展开讲解如何将查询、变更与 fragment 在运行时遇到的字段错误field error、required(action: THROW)失败以及缺失数据missing data等异常状态从默认的静默 null或throwOnFieldError下的运行时异常转变为显式出现在响应数据中的错误对象。读完本文你将掌握catch的to参数语义、错误冒泡规则、与 Semantic Nullability 及throwOnFieldError的联动行为并了解其在 Relay 编译器与运行时中的底层实现与测试验证。一、为什么需要catch让错误从隐形变为可见在 GraphQL 中当服务端某个字段的 resolver 抛出异常时协议规定该字段返回null并将错误信息放入独立的errors数组。默认情况下Relay 会把这个字段读成null错误信息对组件代码而言是隐形的——组件拿到null时无法区分服务器真的返回了 null还是服务器执行出错。catch指令正是为此设计它可以被添加到字段、fragment/operation 定义、或带别名的内联 fragment 展开aliased inline fragment spread上用于声明运行时遇到的异常与意外值应该如何被处理。使用catch后Relay 会把错误状态作为 fragment/query/mutation 数据的一部分暴露出来而不是像过去那样返回一个无差别的null也不是像使用throwOnFieldError那样直接抛出一个 JavaScript 异常。这就让开发者可以在组件层对字段错误做细粒度、显式的处理。文档原文将catch定位为一种声明式错误处理策略它回答的是当这个子树内出现错误时我该如何拿到错误信息的问题而不是如何吞掉错误的问题。二、to参数RESULT与NULL两种处理策略catch接受一个可选的to参数取值有二to取值行为语义RESULT默认值字段值以{ ok: true, value: T } \| { ok: false, errors: [error] }的形式返回适合实现字段粒度field-granular的显式错误处理逻辑NULL若catch范围内出现错误字段值被替换为null适合只想要旧行为返回 null但希望错误被记录/不抛异常的场景RESULT是默认值即使你在catch后面不带任何参数Relay 也会按RESULT语义处理。这一点在编译器源码中有明确印证——compiler/crates/relay-transforms/src/catch_directive.rs中的catch_to_with_fallback函数当catch_to为None时直接返回CatchTo::Resultpub fn catch_to_with_fallback(catch_to: OptionCatchTo) - CatchTo { match catch_to { Some(to) to, // catch without an argument is always RESULT None CatchTo::Result, } }同文件中还定义了完整的枚举与参数名常量pub static CATCH_DIRECTIVE_NAME: LazyLockDirectiveName LazyLock::new(|| DirectiveName(intern!(catch))); pub static NULL_TO: LazyLockStringKey LazyLock::new(|| intern!(NULL)); pub static RESULT_TO: LazyLockStringKey LazyLock::new(|| intern!(RESULT)); pub static TO_ARGUMENT: LazyLockArgumentName LazyLock::new(|| ArgumentName(intern!(to))); #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Debug, Hash)] pub enum CatchTo { Null, Result, }如果传入的to值既不是NULL也不是RESULT编译器会直接 panic 并提示unknown catch to value. Use NULL or RESULT (default) instead.见FromStringKey for CatchTo的实现因此实际使用中只存在这两种合法取值。三、错误在哪里被捕获字段级捕获与祖先级冒泡3.1 直接标注在出错字段上如果catch直接加在错误起源的字段上错误就挂在该字段的返回值上。文档给出的示例query MyQuery { viewer { name catch age } }如果name字段发生了错误响应数据会是这样{ viewer: { name: { ok: false, errors: [{path: [viewer, name]}] } age: 39 } }注意age字段不受影响仍然返回正常值——这是字段粒度错误处理的核心价值错误被就地隔离不影响兄弟字段。3.2 标注在祖先节点上错误向上冒泡catch可以标注在字段的祖先节点例如父级对象字段、fragment、operation上。此时如果catch范围内的任意后代字段出错错误会向上冒泡到最近的catch边界而不是就地呈现query MyQuery { viewer catch { name age } }对应的响应数据{ viewer: { ok: false, errors: [{ path: [viewer, name] }] } }可以看到viewer整体变成了一个Result对象name的错误路径[viewer, name]被完整保留便于应用层定位具体出错字段。这种就近捕获模型与 JavaScript 的 try/catch 冒泡直觉一致错误总是被距离最近的catch祖先接住。3.3 可以标注的位置与限制从编译器的CatchableNodetraitcompiler/crates/relay-transforms/src/catch_directive/catchable_node.rs可以看到catch合法的挂载点是标量字段ScalarField对象/链接字段LinkedFieldfragment 定义FragmentDefinitionoperation 定义OperationDefinition内联 fragmentInlineFragment但内联 fragment 有一个硬性限制catch不能用于未加别名的内联 fragment。编译器会抛出Unexpected catch on unaliased inline fragment.错误并提示补上alias见compiler/crates/relay-transforms/src/catch_directive/validation_message.rs。对应的使用方式是在内联 fragment 上同时使用alias例如... catch(to: RESULT) alias(as: myAlias)——这也在运行时测试packages/relay-runtime/store/__tests__/RelayReader-CatchFields-test.js中反复出现。四、对可空性与类型生成的影响与 Semantic Nullability 联动catch的一个关键副作用是改变生成类型的可空性判断。文档明确指出错误被catch显式处理无论是字段自身标注catch还是被catch祖先覆盖的字段其类型将按照 Semantic Nullability 规则生成。也就是说如果服务端 schema 用semanticNonNull标注某字段只在出错时为 null那么在catch的保护下Relay 生成的 Flow/TypeScript 类型会将该字段标记为非空non-nullable——因为错误不再以null形式泄露到类型系统里而是被catch显式接住了。这一点在类型生成源码compiler/crates/relay-typegen/src/write.rs中有直接体现let is_catch typegen_operation .directives .named(*CATCH_DIRECTIVE_NAME) .is_some(); let type_selections visit_selections( ... is_throw_on_field_error || is_catch, );catch与throwOnFieldError在类型生成时走的是同一条客户端侧已处理字段错误路径。此外还有一个细节let coerce_to_nullable has_explicit_catch_to_null(typegen_operation.directives);即当显式使用catch(to: NULL)时因为错误会被替换为null生成类型会被强制转回可空nullable避免产生类型声称非空、运行时空值的谎言。这条规则的实际收益是过去为了防御错误 null 而写的大量required指令变得不再必要类型系统能更真实地反映语义非空字段的契约。五、catch能捕获哪些异常状态5.1 Payload 字段错误Field ErrorsPayload 字段错误指服务端执行某个字段的 resolver 时抛出的异常。按 GraphQL 规范这种情况下服务器必须在对应位置返回null并附带一个独立的errors对象。默认情况下这个错误对组件是隐形的你只看到 null。在字段上加catch后Relay 读取器会把这些错误**内联in-line**地放进响应数据里使错误可见、可处理、不再隐形。从运行时实现看packages/relay-runtime/store/RelayReader.js的_asResult方法会根据收集到的_fieldErrors组装Result对象无错误时返回{ok: true, value}有错误时返回{ok: false, errors: [...]}。错误对象的具体形态因错误类型而异relay_field_payload.errorpayload 字段错误透出服务端错误对象missing_expected_data.*缺失数据呈现为{ path: [...] }例如{ path: [viewer, name] }relay_resolver.error客户端 resolver 错误呈现为带message的错误描述missing_required_field.throwrequired(action: THROW)失败呈现为带message的完整错误说明。5.2required(action: THROW)在catch内被软化如果某个字段带有required(action: THROW)且它的某个祖先带有catch那么required失败时不再抛出异常而是像普通错误一样冒泡到catch边界以同样的方式提供给你query MyQuery { viewer catch { name required(action: THROW) age } }对应数据{ viewer: { ok: false, errors: [{ path: [viewer, name] }] } }这一点在运行时测试中同样有覆盖catch(to: NULL)可以捕获required(action: THROW)并返回 null测试用例catch(to: NULL) catching a required(action: THROW) returns nullcatch(to: RESULT)则把该错误作为Result的错误分支提供。注意catch与required不能同时标注在同一个字段上——编译器会报错catch and required directives cannot be on the same field见validation_message.rs中的CatchDirectiveWithRequiredDirective。required只能作为catch的后代存在。5.3 缺失数据Missing Data当响应中本应存在某个字段值、却因故缺失undefined时该字段被视为缺失数据。这是另一种意外状态。例如Relay 文档在解释为什么会出现 null时提到的一种典型场景是graph relationship change——图形关系变更导致 store 中某条记录的字段引用失效详见 why-null 文档的 graph relationship change 小节。当缺失数据发生在某个catch祖先范围内时它同样会被捕获{ viewer: { ok: false, errors: [{ path: [viewer, name] }] } }运行时测试中这类场景覆盖很全包括query 级缺失数据、fragment 级缺失数据、带别名内联 fragment 内的缺失数据等参见RelayReader-CatchFields-test.js中大量*MissingData*测试用例。六、catch与throwOnFieldError的协同关系throwOnFieldError的作用是在 fragment 或 query 读取过程中遇到字段错误时让 Relay 运行时抛出 JavaScript 异常。而catch表达的则恰恰相反我不想要异常请把错误放进数据对象里——其行为规则与前面各节完全一致包括冒泡到父字段。两者的关系可以总结为throwOnFieldError是全局兜底让未受保护的字段在出错时抛异常避免应用收到无差别的 nullcatch是局部豁免在throwOnFieldError的范围内用catch圈出你想就地处理的子树把异常转为数据catch不依赖throwOnFieldError即使没有throwOnFieldErrorcatch依然会把错误放进数据对象。区别在于throwOnFieldError缺失时catch之外的字段出错依然不会抛异常因为没有开启抛异常的行为只会退化为默认的 null 处理。换言之两者可以组合出三种策略场景错误处理方式都不使用错误字段返回null错误对组件隐形只用throwOnFieldError未受保护字段出错即抛异常throwOnFieldErrorcatch全局抛异常catch圈出的子树改为返回错误数据文档还提示由于throwOnFieldError会让semanticNonNull字段生成非空类型许多既有的required指令会变得多余可借助remove-unnecessary-required-directivescodemod 清理相关内容见 codemods 指南。七、编译器与运行时实现原理7.1 编译期CatchDirectiveTransformcatch的编译处理集中在compiler/crates/relay-transforms/src/catch_directive.rs核心是一个名为CatchDirectiveTransform的 IR transformer。它的工作方式是遍历 operation、fragment、标量字段、链接字段、内联 fragment 等节点通过catch_metadata()解析其上的catch指令与to参数解析逻辑见catchable_node.rs的catch_metadata方法使用to参数的枚举常量值expect_constant().unwrap_enum()对带catch的节点通过add_metadata_directive注入一个内部元数据指令CatchMetadataDirective { to }把这里有一个 catch 边界及采用何种策略固化到编译产物中同时执行两条验证assert_not_with_required禁止同一字段上同时出现catch与required禁止在未加alias的内联 fragment 上使用catch。这个变换会在编译产物中为 reader 节点生成catchTo元数据——运行时在读取时正是依据它来决定行为。相关元数据定义可见packages/relay-runtime/util/ReaderNode.jsreadonly catchTo?: CatchFieldTo。7.2 运行期RelayReader._catchErrors读取数据时错误处理逻辑集中在packages/relay-runtime/store/RelayReader.js的_catchErrors方法约第 429–494 行其文档注释清晰地描述了算法进入catch范围前把当前已累积的字段错误this._fieldErrors暂存到局部变量遍历catch内的 selection 之后调用_catchErrors(value, to, previousFieldErrors)该方法完成三件事按to类型计算返回值——RESULT走_asResult无错误返回{ok: true, value}有错误返回{ok: false, errors}NULL则在存在错误时将值置为null把catch范围内遇到的错误标记为handled确保它们不会进一步触发 reader 抛异常但仍可被日志系统记录把这些已标记 handled 的错误合并回外层字段错误数组保持外层边界对错误状态的可观测性。值得注意的是NULL分支的行为受特性开关ENABLE_CATCH_IGNORE_HANDLED_FIELD_ERRORS控制见packages/relay-runtime/util/RelayFeatureFlags.js默认false开启后只有尚未被内层catch处理的错误才会触发字段置 null从而让内层catch完整消费自己的错误而不影响外层边界关闭时默认则沿用旧行为所有字段错误都参与该边界判定。7.3 测试验证catch的行为在packages/relay-runtime/store/__tests__/RelayReader-CatchFields-test.js中有系统性的测试覆盖包括但不限于标量字段catch(to: NULL)/catch(to: RESULT)的基本行为query、fragment、带别名内联 fragment 三种挂载位置上的错误捕获与冒泡catch捕获required(action: THROW)to: NULL返回 nullto: RESULT返回错误对象catch捕获缺失数据query 级、fragment 级、内联 fragment 级嵌套catch边界、兄弟字段错误、已标记 handled 错误的日志保留等边界情况。这些测试与编译器侧的变换实现、运行时侧的_catchErrors逻辑相互印证构成了catch从语法到行为的完整闭环。八、注意事项与适用边界小结挂载位置只能用于字段、fragment/operation 定义、以及带alias的内联 fragment未别名内联 fragment 上使用会触发编译错误。与required的关系同一字段上二者互斥required(action: THROW)位于catch祖先之内时会转为数据化的错误不再抛异常。to参数仅RESULT与NULL两种取值缺省为RESULTto: NULL会在类型生成时强制字段回到可空类型。与throwOnFieldError的搭配catch可独立使用也可作为throwOnFieldError下的局部异常豁免区。Semantic Nullability 联动处于catch保护下的semanticNonNull字段会按非空类型生成前提是服务端 schema 正确标注语义可空性。关于catch的设计动机与演进Relay 团队曾在 GraphQL Conf 2024 上围绕该指令与Relay 中的显式错误处理做过专题分享感兴趣的读者可以在社区演讲记录中检索回顾。相关文档与源码索引指令对比throwOnFieldError指令指南类型语义Semantic Nullability 指南缺失数据成因why-null 文档编译期实现compiler/crates/relay-transforms/src/catch_directive.rs、catchable_node.rs、validation_message.rs类型生成逻辑compiler/crates/relay-typegen/src/write.rs运行时读取逻辑packages/relay-runtime/store/RelayReader.js特性开关packages/relay-runtime/util/RelayFeatureFlags.js行为测试packages/relay-runtime/store/tests/RelayReader-CatchFields-test.js赞分享前端开发工具【免费下载链接】relayRelay is a JavaScript framework for building>项目地址https://gitcode.com/gh_mirrors/relay29/relay点击查看免费下载相关推荐Relay throwOnFieldError 指令完全指南让字段级错误从静默为 null变为显式抛错Relay throwOnFieldError 指令完全指南让字段级错误从静默为 null变为显式抛错 throwOnFieldError 是 R前端开发工具Relay 字段级错误处理实战useMutationAction_EXPERIMENTAL 中可空字段返回 null 与 catch(to: RESULT) 检测方案Relay 字段级错误处理实战useMutationAction_EXPERIMENTAL 中可空字段返回 null 与 catch to: RESULT前端开发工具Relay 的 catch 指令实战指南把 GraphQL 字段错误内联进响应数据Relay 的 catch 指令实战指南把 GraphQL 字段错误内联进响应数据 catch 是 Relay 提供的显式错误处理指令它改变了「字段出错前端开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。