ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

t3code 中基于 Effect 的声明式错误处理实战:Schema.TaggedError、catchTag/catchTags 与 catchReason 全解析

t3code 中基于 Effect 的声明式错误处理实战:Schema.TaggedError、catchTag/catchTags 与 catchReason 全解析 AI Agent代码智能体后端前端移动开发桌面应用【免费下载链接】t3code项目地址https://gitcode.com/GitHub_Trending/t3/t3code点击查看免费下载本指南以.repos/effect-smol/ai-docs中01_effect/04_errors错误处理专题为骨架结合 t3code 仓库内大量真实源码checkpointing、auth、assets、web 状态层等展开。读完你将掌握如何用Schema.TaggedError定义类型化领域错误、如何用Effect.catchTag/Effect.catchTags按标签精准捕获、如何用Effect.catchReason/unwrapReason处理带reason细分字段的错误以及 t3code 项目实际采用的错误处理工程规范。一、为什么 t3code 选择 Effect 的类型化错误方案传统 try/catch 的问题在于错误信息是运行时字符串类型系统完全无法感知某个函数会失败、且可能以哪几种方式失败。t3code 的服务端大量基于 Effect仓库apps/server、apps/web、packages均重度依赖构建其核心思想是把失败作为类型系统的一部分每个 Effect 的类型签名同时描述成功值与错误集Effect.Effectnumber, ParseError | ReservedPortError表示成功返回 number可能失败于 ParseError 或 ReservedPortError错误通过_tag字段打上可辨识的标签于是捕获逻辑不必用instanceof或字符串匹配而是按标签结构化分发未被捕获的错误会保留在错误通道中继续向上传播绝不静默吞掉。这正是.repos/effect-smol/ai-docs/src/01_effect/04_errors/index.md专题配套三个示例文件01_error-handling.ts、10_catch-tags.ts、20_reason-errors.ts要解决的核心问题。二、用 Schema.TaggedError 定义领域错误错误处理的第一步是把领域内每一种失败显式建模为独立的错误类型。ai-docs 示例使用Schema.TaggedError同时获得类型化字段 _tag标签 Schema 校验三合一能力import { Effect, Schema } from effect // Define custom errors using Schema.TaggedError export class ParseError extends Schema.TaggedErrorParseError()(ParseError, { input: Schema.String, message: Schema.String }) {} export class ReservedPortError extends Schema.TaggedErrorReservedPortError()(ReservedPortError, { port: Schema.Int }) {}要点Schema.TaggedErrorX()(TagName, { fields })的第一个泛型参数是类自身第一个运行时参数是_tag的值与类名保持一致是惯例字段用Schema 描述而非普通 TS 类型Schema.String、Schema.Int不仅提供类型推导还可在反序列化/校验场景复用例如把错误写入日志或跨进程传输时用Schema.encode/Schema.decode每个错误实例自动携带_tag例如ReservedPortError实例的_tag ReservedPortError字段port可直接访问。t3code 中的真实建模checkpointing/Errors.ts仓库里apps/server/src/checkpointing/Errors.ts是教科书式的 TaggedError 用法为检查点 diff 服务定义了 5 个独立错误每个错误不仅声明 Schema 字段还通过override get message()派生可读的错误消息例如export class CheckpointDiffResultInvalidError extends Schema.TaggedErrorCheckpointDiffResultInvalidError()( CheckpointDiffResultInvalidError, { operation: CheckpointDiffOperation, threadId: ThreadId, }, ) { override get message(): string { const result this.operation CheckpointDiffQuery.getTurnDiff ? turn diff : full thread diff; return Checkpoint invariant violation in ${this.operation}: Computed ${result} result does not satisfy contract schema.; } }并在文件末尾用联合类型汇总服务级错误面export type CheckpointServiceError | CheckpointStoreError | ProjectionRepositoryError | CheckpointDiffResultInvalidError | CheckpointThreadNotFoundError | CheckpointWorkspacePathMissingError | CheckpointTurnRangeUnavailableError | CheckpointRefUnavailableError;这种细粒度错误类 顶层级联错误联合的模式在apps/server/src/auth/EnvironmentAuth.ts中同样被大量使用——该文件定义了 30 余个Schema.TaggedErrorServerAuthBootstrapCredentialValidationError、ServerAuthInvalidCredentialError、ServerAuthMcpApprovalCodeError等且共享cause: Schema.Defect()上下文以保留底层异常链。可见一个模块一个错误集文件、错误类只建模字段、message 用 getter 派生已是 t3code 服务端的事实标准。三、按标签捕获Effect.catchTag 与多标签数组定义好错误后下一步是在调用点按标签捕获。ai-docs 第一个示例展示了Effect.catchTag的两种形态declare const loadPort: (input: string) Effect.Effectnumber, ParseError | ReservedPortError export const recovered loadPort(80).pipe( // Catch multiple errors with Effect.catchTag, and return a default port number. Effect.catchTag([ParseError, ReservedPortError], (_) Effect.succeed(3000)) ) export const withFinalFallback loadPort(invalid).pipe( // Catch a specific error with Effect.catchTag Effect.catchTag(ReservedPortError, (_) Effect.succeed(3000)), // Catch all errors with Effect.catch Effect.catch((_) Effect.succeed(3000)) )关键语义Effect.catchTag(ReservedPortError, handler)精确捕获该标签handler 收到的参数即错误实例可直接读取字段如error.portEffect.catchTag([ParseError, ReservedPortError], handler)数组形式一次捕获多个标签命中任一标签都进入同一 handler未被捕获的标签会继续留在错误通道向上传播类型系统仍会强制调用方处理——这是错误不可能被悄悄吞掉的保证Effect.catch是兜底全捕获处理所有剩余错误。示例withFinalFallback展示了典型的分层策略——先精确处理ReservedPortError再用catch兜住其余如ParseError并回退到默认端口 3000。四、集中分发Effect.catchTags 与对象式处理器当同一段代码可能遇到多个标签、且每个标签需要不同恢复逻辑时Effect.catchTag的链式写法会显得冗长。ai-docs 第二个示例10_catch-tags.ts演示了Effect.catchTags——用一个对象同时注册多个标签的处理器export class ValidationError extends Schema.TaggedErrorValidationError()(ValidationError, { message: Schema.String }) {} export class NetworkError extends Schema.TaggedErrorNetworkError()(NetworkError, { statusCode: Schema.Int }) {} declare const fetchUser: (id: string) Effect.Effectstring, ValidationError | NetworkError export const userOrFallback fetchUser(123).pipe( Effect.catchTags({ ValidationError: (error) Effect.succeed(Validation failed: ${error.message}), NetworkError: (error) Effect.succeed(Network request failed with status ${error.statusCode}) }) )注意字段访问的细节ValidationError的 handler 读取error.messageNetworkError的 handler 读取error.statusCode——每个 handler 的参数类型都会被精确收窄这是catchTags与手写switch (error._tag)相比最大的类型安全优势。t3code 中的真实用法web 状态层与资产访问apps/web/src/state/desktopUpdate.ts在读取桌面端更新状态失败时把错误转换为日志并回退为null用的是对象式catchTagsyield* Effect.tryPromise({ try: () bridge.getUpdateState(), catch: (cause) new DesktopUpdateStateReadError({ attemptCount: INITIAL_STATE_READ_ATTEMPT_COUNT, cause }), }).pipe( Effect.retry({ times: INITIAL_STATE_READ_ATTEMPT_COUNT - 1 }), Effect.catchTags({ DesktopUpdateStateReadError: (error) Effect.logError(error.message, { error, errorTag: error._tag, attemptCount: error.attemptCount, }).pipe(Effect.as(null)), }), );这里还展示了组合套路tryPromise把桥接层异常转成领域错误 →retry有限重试 →catchTags收尾降级错误标签errorTag一并写入日志便于观测。apps/server/src/assets/AssetAccess.ts中也有典型例子——把PlatformError的NotFound子原因转成Optionconst optionOnNotFound A, R( effect: Effect.EffectA, PlatformError.PlatformError, R, ): Effect.EffectOption.OptionA, PlatformError.PlatformError, R effect.pipe( Effect.asSome, Effect.catchTags({ PlatformError: (error) error.reason._tag NotFound ? Effect.succeed(Option.noneA()) : Effect.fail(error), }), );t3code 的 Lint 规范强制用 catchTags 而非 catchTag值得特别指出t3code 自带 oxlint 插件规则 prefer-catch-tags.ts其报错信息明确写道Catch known tags with Effect.catchTags({ Tag: handler }), even for one tag.该规则会扫描import { catchTag } from effect/Effect的命名导入以及通过import * as Effect from effect/Effect命名空间访问的Effect.catchTag成员表达式一旦发现就建议改用Effect.catchTags。这意味着即便只捕获一个标签t3code 工程规范也要求统一使用对象式catchTags以保持代码风格一致、便于日后扩展多个标签。配套测试见 prefer-catch-tags.test.ts。五、错误内的细分原因reason 字段与 catchReason / unwrapReason有时一个领域错误如AI 调用失败之下还有多个细分原因限流、配额不足、安全拦截。ai-docs 第三个示例20_reason-errors.ts展示了用Schema.Union建模嵌套原因再用catchReason系列组合子处理的进阶技巧export class RateLimitError extends Schema.TaggedErrorRateLimitError()(RateLimitError, { retryAfter: Schema.Finite }) {} export class QuotaExceededError extends Schema.TaggedErrorQuotaExceededError()(QuotaExceededError, { limit: Schema.Int }) {} export class SafetyBlockedError extends Schema.TaggedErrorSafetyBlockedError()(SafetyBlockedError, { category: Schema.String }) {} export class AiError extends Schema.TaggedErrorAiError()(AiError, { reason: Schema.Union([RateLimitError, QuotaExceededError, SafetyBlockedError]) }) {} declare const callModel: Effect.Effectstring, AiError此时AiError.reason是一个联合类型reason._tag会进一步区分是哪种原因。5.1 单一原因处理Effect.catchReasonexport const handleOneReason callModel.pipe( Effect.catchReason( AiError, // The parent error _tag to catch RateLimitError, // The reason _tag to catch (reason) Effect.succeed(Retry after ${reason.retryAfter} seconds), // 可选的兜底处理该父错误下其余所有原因 (reason) Effect.succeed(Model call failed for reason: ${reason._tag}) ) )catchReason的前两个参数分别是父错误标签与原因标签命中后 handler 直接收到被解包后的reason例如RateLimitError可直接读reason.retryAfter最后一个可选参数是所有未命中原因的 catch-all 处理器。5.2 多原因集中处理Effect.catchReasonsexport const handleMultipleReasons callModel.pipe( Effect.catchReasons( AiError, { RateLimitError: (reason) Effect.succeed(Retry after ${reason.retryAfter} seconds), QuotaExceededError: (reason) Effect.succeed(Quota exceeded at ${reason.limit} tokens) } // 可选的 catch-all // (reason) Effect.succeed(Unhandled reason: ${reason._tag}) ) )catchReasons与catchTags的对象式分发一致但作用对象是同一父错误下的多个原因标签。5.3 把原因提升进错误通道Effect.unwrapReason第三种思路是先把 reason 解包成独立错误再复用前面学过的 catchTags让两套处理体系统一起来export const unwrapAndHandle callModel.pipe( Effect.unwrapReason(AiError), Effect.catchTags({ RateLimitError: (reason) Effect.succeed(Back off for ${reason.retryAfter} seconds), QuotaExceededError: (reason) Effect.succeed(Increase quota beyond ${reason.limit}), SafetyBlockedError: (reason) Effect.succeed(Blocked by safety category: ${reason.category}) }) )Effect.unwrapReason(AiError)会把AiError中的reason实例投影到错误通道顶层使下游错误联合类型变为RateLimitError | QuotaExceededError | SafetyBlockedError——此后catchTags、catch等所有组合子都能直接作用于这些细分原因。当父错误只有一个reason字段时这是最推荐的方式因为它把嵌套错误压平让类型与处理代码都保持扁平。t3code 中的真实用法antigravityAuthSupport.tsapps/server/src/provider/antigravityAuthSupport.ts中读取符号链接前需要容忍文件不存在的NotFound原因并转成undefined正是catchReason的实战const existing yield* fs.readLink(link).pipe( Effect.map((value): string | undefined path.resolve(path.dirname(link), value)), Effect.catchReason(PlatformError, NotFound, () Effect.undefined), );与上文AssetAccess.ts的catchTags({ PlatformError: ... })内手写error.reason._tag NotFound相比catchReason(PlatformError, NotFound)是更简洁的等价表达——同样处理父错误 细分原因两层结构但由框架替你完成原因匹配与解包。六、组合成完整策略分层捕获 兜底 有限重试综合 ai-docs 三个示例与 t3code 源码一套可复用的错误处理范式可以归纳为四层建模层为每个领域失败定义Schema.TaggedError字段只放结构化数据message用 getter 派生跨层调用用联合类型汇总错误面参考 Errors.ts 的CheckpointServiceError精准恢复层在业务调用点用Effect.catchTags按标签分发不同的降级/回退逻辑参考 desktopUpdate.ts兜底层对确实无法逐类处理的错误用Effect.catch统一兜底如withFinalFallback回退默认端口 3000重试与观测Effect.retry配合标签化错误做有限次重试并把error._tag、关键字段写入结构化日志让错误可被搜索与聚合。错误处理完成后Effect 会把剩余未处理错误继续沿错误通道传播直到main/runMain边界统一收口——这正是 Effect 模型相对 try/catch 的核心价值失败路径在类型层面全程可见、可追踪、可组合。七、延伸阅读完整错误处理示例.repos/effect-smol/ai-docs/src/01_effect/04_errors/下的 01_error-handling.ts、10_catch-tags.ts、20_reason-errors.tst3code 的 Lint 强制规范prefer-catch-tags.ts领域错误建模范例checkpointing/Errors.ts、auth/EnvironmentAuth.ts错误恢复实战AssetAccess.ts、antigravityAuthSupport.ts、desktopUpdate.ts运行与启动错误收口.repos/effect-smol/ai-docs/src/01_effect/06_running/10_run-main.ts、20_layer-launch.ts以及观测主题.repos/effect-smol/ai-docs/src/01_effect/08_observability/日志与 OTLP Tracing。赞分享AI Agent代码智能体后端前端移动开发桌面应用【免费下载链接】t3code项目地址https://gitcode.com/GitHub_Trending/t3/t3code点击查看免费下载相关推荐t3code 中的 effect/sql-sqlite-node基于 node:sqlite 的 Effect SQL 客户端全解析t3code 中的 effect/sql sqlite node基于 node:sqlite 的 Effect SQL 客户端全解析 导读 本文以 t3coAI Agent代码智能体后端前端移动开发桌面应用Envoy Mobile 公开 API 完全指南Engine 启动、HTTP/gRPC 流式请求与 Pulse 指标采集Envoy Mobile 公开 API 完全指南Engine 启动、HTTP/gRPC 流式请求与 Pulse 指标采集 Envoy Mobile 将云原生代云原生服务网格网络微服务Hindsight Codex 记忆银行策略按仓库隔离分库把跨仓库召回噪声降为零Hindsight Codex 记忆银行策略按仓库隔离分库把跨仓库召回噪声降为零 用 Hindsight 给 Codex 配记忆时默认所有仓库的会话都写进人工智能AI AgentAgent 记忆MCP 服务创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进