
Relay 的 catch 指令实战指南把 GraphQL 字段错误内联进响应数据【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relaycatch是 Relay 提供的显式错误处理指令它改变了「字段出错时一律返回 null」的默认行为让异常和意外值以{ ok: true, value } | { ok: false, errors }的结构直接出现在你的查询、Fragment 或 Mutation 的响应数据中。本文以 Relay v18 官方指南为主线结合本仓库的编译器源码relay-transforms与运行时实现RelayReader完整讲解catch的语法、错误冒泡规则、与required/throwOnFieldError的协同方式以及to参数的两种取值读完即可在实际项目中对字段级错误做显式、细粒度的处理。catch 是什么从「静默 null」到「显式错误」在 GraphQL 中当服务端执行某个字段的 resolver 时抛出了异常规范要求服务端在该字段位置返回null同时在响应顶层的errors数组中单独记录这条 field error。这意味着默认情况下Relay 读到的是「数据变空了」而真正的原因异常本身被隐藏起来开发者在组件里往往无法区分「这真的是 null」还是「出错导致的 null」。catch指令正是为解决这个问题而生把它加到 Relay 的 query / fragment / mutation 中的字段上即可声明「这个字段的异常和意外值应该如何在运行时被处理」。加上catch之后Relay 会在响应数据中把异常直接呈现给你而不是静默地给一个null。具体来说当 GraphQL 响应中包含 field errors 时Relay 会主动查找这些错误如果错误所在的字段、或其祖先字段上带有catch指令Relay 就会把该字段的响应数据替换为下面两种形状之一成功{ ok: true, value: your value }失败{ ok: false, errors: [...] }从当前仓库源码看catch的作用范围不止于普通字段——compiler/crates/relay-transforms/src/catch_directive/catchable_node.rs中定义了CatchableNodetrait并为ScalarField标量字段、LinkedField对象字段、FragmentDefinitionFragment 定义、OperationDefinition操作定义以及InlineFragment内联片段统一实现了该 trait意味着这些节点上都可以挂载catch。而 最新版本文档 也明确指出catch可以添加到字段、fragment/operation 定义或带别名的内联片段展开aliased inline fragment spreads上。两种捕获模式字段直接捕获与祖先冒泡直接捕获错误停留在出错字段本身当catch直接标注在出错的字段上时错误就停留在该字段例如query MyQuery { viewer { name catch age } }如果name字段在服务端执行时出错Relay 会把它捕获到name字段自身的数据上{ viewer: { name: { ok: false, errors: [ { message: Couldnt get name, path: [viewer, name] } ] } age: 39 } }注意age字段不受影响仍然正常返回 39。这就是「字段粒度field-granular的错误处理」——你可以在组件里只针对name的错误做降级展示而其余数据照常使用。祖先捕获错误冒泡到最近的catch祖先如果catch位于某个字段的祖先节点上错误不会停留在子字段而是沿选择集向上冒泡到最近的catch祖先query MyQuery { viewer catch { name age } }此时name出错错误会冒泡到viewer{ viewer: { ok: false, errors: [ { message: Couldnt get name, path: [viewer, name] } ] } }冒泡时错误对象中的path依然精确记录错误真正的来源[viewer, name]而ok: false出现在捕获它的祖先viewer上。这种模式非常适合「整块数据要么整体可用、要么整体给出原因」的场景比如卡片、列表项这类以对象为单位的 UI。三类可以被 catch 捕获的错误Payload Field Errors服务端字段执行异常Payload errors 是服务端执行某个字段的 resolver 时发生异常而产生的错误。这种情况下GraphQL 服务端会在本应返回值的位置放一个null并在独立的errors对象中记录详情。当你在字段上使用catch时Relay 会把这些错误从「隐藏在顶层 errors 中」转变为「内联在该字段的数据中」让它们不再不可见、也更容易被处理。这里有一个重要的副产品可空nullable字段现在可以区分「真 null」和「异常 null」了。因为捕获后的形状要么是{ ok: true }且value为null真 null要么是{ ok: false }并携带真实错误异常导致的 null两者的数据形态完全不同不再混淆。required(action: THROW) 位于 catch 之下从抛异常改为冒泡required用于声明字段不可缺失。当required(action: THROW)与一个带有catch的祖先同时出现时原本「抛出 JavaScript 异常」的行为会被改写——required的错误不再抛出而是像普通错误一样向上冒泡被catch捕获并提供在数据中query MyQuery { viewer catch { name required(action: THROW) age } }此时如果viewer.name缺失响应数据会是{ viewer: { ok: false, errors: [ { message: Relay: Missing required value at path viewer.name in MyQuery., } ] } }注意错误消息直接来自 Relay 运行时Relay: Missing required value at path viewer.name in MyQuery.。需要特别区分的是required可以出现在catch的子孙位置错误冒泡进catch但两者不能标注在同一个字段上。这一点在编译器源码中有明确的校验compiler/crates/relay-transforms/src/catch_directive.rs的assert_not_with_required方法会在同一节点同时携带catch与required时抛出诊断错误对应catch_directive/validation_message.rs中的CatchDirectiveWithRequiredDirective消息为 catchandrequireddirectives cannot be on the same field。Missing Data响应数据缺失还有一种意外状态也会被catch捕获字段本应返回一个值但响应中该字段是undefined例如 schema 中的对象关系发生了图结构变化客户端对不上号详见 Relay 文档中「why null」关于 graph relationship change 的说明。当这种缺失发生且存在catch祖先时它同样会被捕获{ viewer: { ok: false, errors: [ { message: Relay: Missing data for one or more fields in MyQuery, } ] } }此时 Relay 给出的错误消息为Relay: Missing data for one or more fields in MyQuery。catch 与 throwOnFieldError 如何协同throwOnFieldError是一个让字段在发生 field error 时直接抛出 JavaScript 异常的指令全局开启后所有字段都会 throw。而catch的作用正好相反——它明确告诉 Relay「这个位置不要抛 JavaScript 异常请把错误放进数据对象里」并且遵循上面列出的全部规则包括冒泡到父字段。两个要点值得牢记catch不依赖throwOnFieldError也能生效。即使没有开启throwOnFieldErrorcatch依然会把错误提供到数据对象中。区别在于此时catch之外的其他字段在出错时依然不会 throw——因为缺少throwOnFieldError的全局开启它们仍走「返回 null」的默认路径。作用范围是局部的。无论catch还是throwOnFieldError都只处理它们所在的 query / fragment / mutation 之内的字段错误不处理任何通过 fragment spread 引入的字段错误——这是当前仓库 官方指南 中特别提示的行为边界。to 参数RESULT 与 NULLcatch接受一个可选的to参数用于选择错误呈现方式共有两种取值。to: RESULT默认值catch(to: RESULT)启用本文前述的全部行为为自身及子字段中出错的位置提供内联错误即{ ok: true, value: T } | { ok: false, errors: [error] }。由于 RESULT 是默认值catch与catch(to: RESULT)写法完全等价。这也是源码中的默认兜底逻辑——catch_directive.rs的catch_to_with_fallback函数明确指出catch不带参数时恒为RESULT。to: NULLcatch(to: NULL)则恢复catch出现之前的行为字段出错时该字段的值就是null。它保留了「错误仍然会被观察到并被标记为已处理」的内部语义但对应用层暴露的形态与默认行为一致——字段出错即置空。编译器源码中to参数被建模为CatchTo枚举Null/Result两个变体并通过FromStringKey for CatchTo把 GraphQL 枚举字面量NULL/RESULT映射到对应变体如果遇到其它取值会直接 panic 提示「UseNULLorRESULT(default) instead」。因此实际项目中请只使用这两个合法取值。编译器如何实现 catch源码级拆解catch的编译期处理位于compiler/crates/relay-transforms/src/catch_directive.rs是整个转换管线的独立一环。核心逻辑如下指令名与参数名CATCH_DIRECTIVE_NAME catch、TO_ARGUMENT to、合法枚举值NULL_TO NULL、RESULT_TO RESULT。转换器CatchDirective实现Transformertrait逐节点遍历程序。对OperationDefinition、FragmentDefinition、ScalarField、LinkedField、带别名的InlineFragment只要检测到catch就会在保留原指令的同时追加一条内部元数据指令CatchMetadataDirective携带解析后的CatchTo供后续 codegen 阶段读取。错误累积转换过程中的非法用法不会直接中止编译而是收集进errors列表只有存在错误时才返回Err(diagnostics)由上层统一报告。编译期校验规则同一字段同时使用catch与required→ 报错CatchDirectiveWithRequiredDirective。在未带别名的内联片段上使用catch→ 报错CatchNotValidOnUnaliasedInlineFragment提示需配合... alias使用。因为未别名化的内联片段会原样合入父级选择集无法独立承载错误边界而带别名alias的内联片段则可以成为独立的catch边界。嵌套捕获转换会递归处理子选择集因此内层catch会先于外层生效错误被最近的catch边界截获不会无限向外传播。值得补充的是编译器还提供了「client schema 扩展中使用catch」的专项校验见compiler/crates/relay-transforms/src/validations/validate_client_schema_extensions_use_catch.rs说明catch在客户端扩展字段上也受到支持与约束。运行时如何处理 catchRelayReader 与特性开关编译产物到达运行时后由packages/relay-runtime/store/RelayReader.js负责实际的错误捕获。核心方法是_catchErrors约在RelayReader.jsL446 起读取字段/Fragment/操作上的metadata.catchTo由编译期CatchMetadataDirective写入在进入标注了catch的选择集之前先记录现场遍历完成后收集该范围内发生的 field errors将错误标记为「已处理handled」避免它们向上继续触发 reader 抛出或影响外层边界把错误与捕获位置合并最终按CatchTo的取值组装成{ ok, value }或{ ok, errors }或直接置null。此外packages/relay-runtime/util/RelayFeatureFlags.js中还有一个相关特性开关ENABLE_CATCH_IGNORE_HANDLED_FIELD_ERRORS默认关闭。从其注释可以读到设计意图开启后外层catch边界会忽略已经被内层catch处理过的错误而在未开启时即使错误已被内层catch接住外层catch(to: NULL)仍可能把字段置空、外层catch(to: RESULT)仍会报告{ok: false}。这个开关用于收紧嵌套catch场景下的传播语义。用编译器的测试用例验证 catch 行为仓库为catch提供了丰富的 fixture 测试位于compiler/crates/relay-transforms/tests/catch_directive/fixtures/每对.graphql输入与.expected输出都验证了一次完整的转换结果覆盖操作与 Fragment 级别catch-usage-query.graphql、catch-usage-query-mutation.graphql、catch-usage-fragment.graphql、catch-usage-on-query.graphql——验证catch挂在 query、mutation、fragment 定义上的转换输出字段级别catch-usage-linked.graphql、catch-usage-linked-with-linked-sibling.graphql——验证对象linked字段上的catch以及同级字段并存时的行为嵌套捕获catch-usage-nested-catches.graphql——验证多层catch嵌套时的边界与传播别名内联片段catch-usage-inline-fragment-with-alias.graphql、catch-to-default-usage-inline-fragment-with-alias.graphql——验证catch与alias内联片段的合法组合默认参数catch-to-default-usage-query.graphql——验证不带to参数时等价于to: RESULT。同时还有一批.invalid用例专门验证编译期校验例如catch-usage-on-query-with-required.invalid.graphql同节点catchrequired冲突、catch-usage-inline-fragment-no-alias.invalid.graphql未别名内联片段、catch-usage-fragment-spread-alias.invalid.graphql/catch-usage-fragment-spread-no-alias.invalid.graphqlfragment spread 上的catch不被支持——这些用例印证了「catch不能用于 fragment spread」的限制。运行时侧也有对应的行为测试例如packages/relay-runtime/store/__tests__/RelayReader-CatchFields-test.js其中包含CatchToResultFragment、CatchToNullFragment、CatchMissingInQueryToResultErrorQuery等生成用例分别覆盖to: RESULT、to: NULL以及「缺失数据」的读取行为。实践建议与注意事项选择合适的捕获粒度错误只影响单个字段时把catch放在该字段上实现字段级降级整块 UI 需要整体兜底时放在祖先对象上利用冒泡机制用path定位具体出错字段。善用ok判别可空语义捕获后{ ok: true, value: null }与{ ok: false, errors }形态分明可以放心区分「真正的空值」与「异常」。不要在同一字段混用catch与required编译器会直接报错请把required放在catch的子孙字段上让其错误冒泡进catch边界。认清作用边界catch只覆盖它所在操作内直接书写的字段不覆盖 spread fragment 内部的字段错误若项目使用throwOnFieldError全局抛出用catch显式声明「此处不抛出」的例外。理解嵌套传播默认特性开关下内层catch已处理的错误仍可能影响外层catch的判定如需「内层已接住、外层不再受影响」的语义可关注ENABLE_CATCH_IGNORE_HANDLED_FIELD_ERRORS特性开关。参考阅读最新版catch指南website/docs/guides/catch-directive.mdx编译器转换实现compiler/crates/relay-transforms/src/catch_directive.rs、catchable_node.rs、validation_message.rs编译器测试 fixturescompiler/crates/relay-transforms/tests/catch_directive/fixtures运行时错误捕获packages/relay-runtime/store/RelayReader.js、packages/relay-runtime/store/tests/RelayReader-CatchFields-test.js相关特性开关packages/relay-runtime/util/RelayFeatureFlags.js【免费下载链接】relayRelay is a JavaScript framework for building>项目地址: https://gitcode.com/gh_mirrors/relay29/relay创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考