- AI Agent
- 代码智能体
- 后端
- 前端
- 移动开发
- 桌面应用
【免费下载链接】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.Effect<number, 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.TaggedError<ParseError>()("ParseError", { input: Schema.String, message: Schema.String }) {} export class ReservedPortError extends Schema.TaggedError<ReservedPortError>()("ReservedPortError", { port: Schema.Int }) {}要点:
Schema.TaggedError<X>()("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.TaggedError<CheckpointDiffResultInvalidError>()( "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.TaggedError(ServerAuthBootstrapCredentialValidationError、ServerAuthInvalidCredentialError、ServerAuthMcpApprovalCodeError等),且共享cause: Schema.Defect()上下文以保留底层异常链。可见"一个模块一个错误集文件、错误类只建模字段、message 用 getter 派生"已是 t3code 服务端的事实标准。
三、按标签捕获:Effect.catchTag 与多标签数组
定义好错误后,下一步是在调用点按标签捕获。ai-docs 第一个示例展示了Effect.catchTag的两种形态:
declare const loadPort: (input: string) => Effect.Effect<number, 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.port);Effect.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.TaggedError<ValidationError>()("ValidationError", { message: Schema.String }) {} export class NetworkError extends Schema.TaggedError<NetworkError>()("NetworkError", { statusCode: Schema.Int }) {} declare const fetchUser: (id: string) => Effect.Effect<string, 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.message,NetworkError的 handler 读取error.statusCode——每个 handler 的参数类型都会被精确收窄,这是catchTags与手写switch (error._tag)相比最大的类型安全优势。
t3code 中的真实用法:web 状态层与资产访问
apps/web/src/state/desktopUpdate.ts在读取桌面端更新状态失败时,把错误转换为日志并回退为null,用的是对象式catchTags:
yield* 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子原因转成Option:
const optionOnNotFound = <A, R>( effect: Effect.Effect<A, PlatformError.PlatformError, R>, ): Effect.Effect<Option.Option<A>, PlatformError.PlatformError, R> => effect.pipe( Effect.asSome, Effect.catchTags({ PlatformError: (error) => error.reason._tag === "NotFound" ? Effect.succeed(Option.none<A>()) : 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.TaggedError<RateLimitError>()("RateLimitError", { retryAfter: Schema.Finite }) {} export class QuotaExceededError extends Schema.TaggedError<QuotaExceededError>()("QuotaExceededError", { limit: Schema.Int }) {} export class SafetyBlockedError extends Schema.TaggedError<SafetyBlockedError>()("SafetyBlockedError", { category: Schema.String }) {} export class AiError extends Schema.TaggedError<AiError>()("AiError", { reason: Schema.Union([RateLimitError, QuotaExceededError, SafetyBlockedError]) }) {} declare const callModel: Effect.Effect<string, AiError>此时AiError.reason是一个联合类型,reason._tag会进一步区分是哪种原因。
5.1 单一原因处理:Effect.catchReason
export 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.catchReasons
export 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.ts
apps/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.ts; - t3code 的 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
相关推荐
t3code 中的 @effect/sql-sqlite-node:基于 node:sqlite 的 Effect SQL 客户端全解析
t3code 中的 @effect/sql sqlite node:基于 node:sqlite 的 Effect SQL 客户端全解析 导读 本文以 t3co
AI 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),仅供参考