effect-advanced
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseEffect Advanced: Patterns, Conventions & Pitfalls
Effect 高级教程:模式、约定与常见陷阱
This skill defines the rules, conventions, and architectural decisions for building
production Effect-TS applications. It is intentionally opinionated to prevent common
pitfalls and enforce patterns that scale.
For detailed API documentation, use other appropriate tools (documentation lookup,
web search, etc.) — this skill focuses on how and why to use Effect idiomatically,
not the full API surface.
本技能定义了构建生产级 Effect-TS 应用的规则、约定和架构决策。它特意采用了强约束的设计,目的是避免常见陷阱,推行可扩展的开发模式。
如需详细的 API 文档,请使用其他合适的工具(文档查询、网页搜索等)——本技能聚焦于如何以及为什么要按惯用方式使用 Effect,而非完整的 API 说明。
Table of Contents
目录
Core Conventions
核心约定
Use Effect.gen
for business logic
Effect.gen业务逻辑使用 Effect.gen
Effect.genGenerators read like synchronous code and are strongly preferred over long /
chains for anything beyond trivial composition:
.pipe.flatMaptypescript
const program = Effect.gen(function* () {
const config = yield* ConfigService;
const user = yield* UserRepo.findById(config.userId);
return user;
});Reserve for data transformation pipelines and short combinator chains.
pipe生成器的读感与同步代码类似,除了非常简单的组合逻辑之外,都强烈建议优先使用生成器,而非冗长的 / 调用链:
.pipe.flatMaptypescript
const program = Effect.gen(function* () {
const config = yield* ConfigService;
const user = yield* UserRepo.findById(config.userId);
return user;
});请将 保留用于数据转换管道和短组合子调用链。
pipeNever throw — use Effect's error channel
不要主动抛出错误 —— 使用 Effect 的错误通道
| Instead of... | Use |
|---|---|
| |
| |
| Callback APIs | |
| Unrecoverable crashes | |
| 不要使用... | 请使用 |
|---|---|
| |
对 promise 使用 | |
| 回调类 API | |
| 不可恢复的崩溃 | |
Functions over methods
优先使用函数而非方法
Prefer over for composability and
tree-shaking. Flat imports () are fine for
applications; namespace imports () are
better for libraries.
Effect.map(e, f)e.pipe(Effect.map(f))import { Effect } from "effect"import * as Effect from "effect/Effect"为了更好的可组合性和 tree-shaking 效果,优先使用 而非 。应用开发中使用扁平导入()即可;库开发更推荐使用命名空间导入()。
Effect.map(e, f)e.pipe(Effect.map(f))import { Effect } from "effect"import * as Effect from "effect/Effect"@effect/schema
is deprecated
@effect/schema@effect/schema
已被弃用
@effect/schemaSchema has been merged into core . Import from directly:
effect"effect"typescript
import { Schema } from "effect";
// NOT: import { Schema } from "@effect/schema"Schema 已经被合并到 核心库中,请直接从 导入:
effect"effect"typescript
import { Schema } from "effect";
// 不要这么写: import { Schema } from "@effect/schema"Use NodeRuntime.runMain
in production
NodeRuntime.runMain生产环境使用 NodeRuntime.runMain
NodeRuntime.runMainEffect.runPromiseSIGINTSIGTERMtypescript
import { NodeRuntime } from "@effect/platform-node";
NodeRuntime.runMain(program.pipe(Effect.provide(AppLayer)));Effect.runPromiseSIGINTSIGTERMtypescript
import { NodeRuntime } from "@effect/platform-node";
NodeRuntime.runMain(program.pipe(Effect.provide(AppLayer)));Error Handling Philosophy
错误处理理念
Failures vs defects — the fundamental distinction
失败 vs 缺陷 —— 核心区别
| Aspect | Failure (expected) | Defect (unexpected) |
|---|---|---|
| API | | |
| Type channel | Tracked in | Never appears in |
| Recovery | | Only at system boundaries |
| Rule of thumb | You intend to handle it at call site | Bug or impossible state |
| 维度 | 失败(预期内) | 缺陷(预期外) |
|---|---|---|
| 对应 API | | |
| 类型通道 | 在 | 永远不会出现在 |
| 恢复方式 | | 仅能在系统边界处处理 |
| 判断标准 | 你打算在调用侧处理它 | 属于 Bug 或者不可能出现的状态 |
Always use tagged errors
始终使用带标签的错误
Plain or string failures miss the value of Effect's typed error channel:
Errortypescript
class UserNotFound extends Data.TaggedError("UserNotFound")<{
readonly id: string;
}> {}
// Tagged errors are yieldable — no Effect.fail wrapper needed
const program = Effect.gen(function* () {
const user = yield* db.findUser(id);
if (!user) yield* new UserNotFound({ id });
return user;
});普通的 或者字符串类型的错误无法发挥 Effect 类型化错误通道的价值:
Errortypescript
class UserNotFound extends Data.TaggedError("UserNotFound")<{
readonly id: string;
}> {}
// 带标签的错误可以直接 yield,不需要包裹 Effect.fail
const program = Effect.gen(function* () {
const user = yield* db.findUser(id);
if (!user) yield* new UserNotFound({ id });
return user;
});catchAll
does NOT catch defects
catchAllcatchAll
不会捕获缺陷
catchAllThis is the #1 error handling mistake:
typescript
Effect.catchAll(program, handler); // catches E only — NOT defects
Effect.catchAllCause(program, handler); // catches everything (E + defects + interrupts)Only use / at system boundaries (top-level error
handlers, HTTP response mappers).
catchAllCausecatchAllDefect这是最常见的错误处理误区:
typescript
Effect.catchAll(program, handler); // 仅捕获 E 类型的错误 —— 不包含缺陷
Effect.catchAllCause(program, handler); // 捕获所有内容(E + 缺陷 + 中断)仅在系统边界处(顶层错误处理器、HTTP 响应映射器)使用 / 。
catchAllCausecatchAllDefectDependency Injection Architecture
依赖注入架构
Service → Layer → Provide (once)
服务 → Layer → 一次注入(Provide)
text
1. Define services with Context.Tag → "what do I need?"
2. Implement via Layers → "how is it built?"
3. Provide once at entry point → "wire it all together"text
1. 使用 Context.Tag 定义服务 → "我需要什么?"
2. 通过 Layer 实现服务 → "它如何构建?"
3. 在入口处一次性注入 → "把所有组件组装起来"Service methods must have R = never
R = never服务方法的 R
必须为 never
RneverDependencies belong in Layer composition, not method signatures:
typescript
// WRONG: leaks dependency to callers
findById: (id: string) => Effect.Effect<User, UserNotFound, Database>;
// RIGHT: Database is wired in the Layer
findById: (id: string) => Effect.Effect<User, UserNotFound>;依赖应该在 Layer 组合中处理,而不是出现在方法签名中:
typescript
// 错误写法: 把依赖泄漏给了调用方
findById: (id: string) => Effect.Effect<User, UserNotFound, Database>;
// 正确写法: Database 在 Layer 中完成组装
findById: (id: string) => Effect.Effect<User, UserNotFound>;Layer composition — know the operators
Layer 组合 —— 了解常用操作符
| Operation | When | Behavior |
|---|---|---|
| Independent services | Both build concurrently |
| A feeds B | upstream builds first |
| Force new instance | Bypasses memoization |
Critical: does NOT sequence construction. If B depends on A, use
, not .
Layer.mergeLayer.provideLayer.merge| 操作 | 使用场景 | 行为说明 |
|---|---|---|
| 独立服务 | 两者并发构建 |
| A 为 B 提供依赖 | 先构建 upstream |
| 强制生成新实例 | 绕过缓存逻辑 |
重要提示: 不会按顺序构建。如果 B 依赖 A,请使用 而非 。
Layer.mergeLayer.provideLayer.mergeOne Effect.provide
at the entry point
Effect.provide在入口处仅调用一次 Effect.provide
Effect.provideScattered calls create hidden dependencies and layer duplication:
providetypescript
// WRONG: provide scattered throughout codebase
const getUser = UserRepo.findById(id).pipe(Effect.provide(DbLayer));
// RIGHT: compose and provide once
const main = program.pipe(Effect.provide(AppLayer));
NodeRuntime.runMain(main);分散的 调用会产生隐藏依赖和 Layer 重复实例:
providetypescript
// 错误写法: provide 调用散落在代码库各处
const getUser = UserRepo.findById(id).pipe(Effect.provide(DbLayer));
// 正确写法: 组合所有 Layer 并在入口处一次性注入
const main = program.pipe(Effect.provide(AppLayer));
NodeRuntime.runMain(main);Resource & Scope Rules
资源与作用域规则
Effect.scoped
is mandatory for acquireRelease
Effect.scopedacquireRelease使用 acquireRelease
时必须搭配 Effect.scoped
acquireReleaseEffect.scopedForgetting is the #1 resource management pitfall — resources
accumulate until the program exits:
Effect.scopedtypescript
// WRONG: scope never closes, connection leaks
const result = yield * getDbConnection;
// RIGHT: scope closes when block completes
const result =
yield *
Effect.scoped(
Effect.gen(function* () {
const conn = yield* getDbConnection;
return yield* conn.query("SELECT 1");
}),
);忘记加 是最常见的资源管理陷阱——资源会一直累积直到程序退出:
Effect.scopedtypescript
// 错误写法: 作用域永远不会关闭,连接泄漏
const result = yield * getDbConnection;
// 正确写法: 代码块执行完成后作用域关闭
const result =
yield *
Effect.scoped(
Effect.gen(function* () {
const conn = yield* getDbConnection;
return yield* conn.query("SELECT 1");
}),
);Release finalizers always run
释放终结器一定会执行
On success, failure, AND interruption — guaranteed. The finalizer receives the
value for conditional cleanup.
Exit无论执行成功、失败还是被中断,终结器都会被执行,这是 Effect 保证的。终结器会收到 值,可以用于实现条件清理逻辑。
ExitMultiple resources in one scope
单个作用域内管理多个资源
typescript
Effect.scoped(
Effect.gen(function* () {
const conn = yield* Effect.acquireRelease(openConn(), closeConn);
const file = yield* Effect.acquireRelease(openFile(), closeFile);
// both released when scope closes, in REVERSE acquisition order
}),
);typescript
Effect.scoped(
Effect.gen(function* () {
const conn = yield* Effect.acquireRelease(openConn(), closeConn);
const file = yield* Effect.acquireRelease(openFile(), closeFile);
// 作用域关闭时两个资源都会被释放,释放顺序和获取顺序相反
}),
);Concurrency Model
并发模型
Prefer high-level APIs over raw fork
优先使用高层 API 而非原始的 fork
| API | Use case |
|---|---|
| Bounded parallel execution |
| Worker pool pattern |
| First to complete wins, others interrupted |
| Deadline on any effect |
Only reach for / when high-level APIs are insufficient.
Effect.forkFiber| API | 适用场景 |
|---|---|
| 有界并行执行 |
| 工作池模式 |
| 取最先完成的结果,其余任务被中断 |
| 给任意 Effect 设置执行截止时间 |
仅当高层 API 无法满足需求时,才使用 / 。
Effect.forkFiberFork variants — know the lifecycle
Fork 变体 —— 了解生命周期差异
| Function | Scope | Cleanup |
|---|---|---|
| Parent's scope | Auto-interrupted with parent |
| Global scope | Nothing cleans it up — you must |
| Nearest Scope | Tied to resource lifecycle |
Gotcha: leaks fibers if you forget to interrupt them.
forkDaemon| 函数名 | 所属作用域 | 清理逻辑 |
|---|---|---|
| 父级作用域 | 父级中断时自动被中断 |
| 全局作用域 | 不会被自动清理 —— 需要开发者手动处理 |
| 最近的作用域 | 与资源生命周期绑定 |
注意: 如果你忘记手动中断 创建的 fiber,会导致 fiber 泄漏。
forkDaemonCommon Pitfalls
常见陷阱
-
Floating effects — creating an Effect without yielding or running it is a silent bug.inside a generator does nothing unless
Effect.log("msg")-ed.yield* -
won't catch defects — use
catchAllat system boundaries for full failure visibility.catchAllCause -
Missing—
Effect.scopedwithout a scope boundary leaks resources until program exit.acquireRelease -
Scattered— compose all layers and provide once at the entry point.
Effect.provide -
Point-free on overloaded functions —silently erases generics. Use explicit lambdas:
Effect.map(myOverloadedFn).Effect.map((x) => myOverloadedFn(x)) -
resume called multiple times — resume must be called exactly once. Multiple calls cause undefined behavior.
Effect.async -
silences errors — converts typed failures to untyped defects. Handle errors properly instead.
orDie -
for dependent services — merge doesn't sequence construction. Use
Layer.mergewhen one layer needs another's output.Layer.provide -
vs
Fiber.join—Fiber.awaitcan cause premature finalizer execution in edge cases. Preferjoinwhen resource safety matters.await -
on infinite streams — never call without a prior
runCollect. It will never terminate and consume unbounded memory.take -
Usingfor scoped tests — effects requiring
it.effectmust useScope, notit.scoped, or you get a type error.it.effect
-
悬浮 Effect —— 创建了 Effect 但没有 yield 或者运行它是一种无报错的 Bug。生成器内部的不会执行,除非你加上
Effect.log("msg")。yield* -
不会捕获缺陷 —— 如需完整的错误可见性,请在系统边界处使用
catchAll。catchAllCause -
遗漏—— 没有作用域边界的
Effect.scoped会导致资源泄漏,直到程序退出才会释放。acquireRelease -
分散的调用 —— 组合所有 Layer,在入口处一次性完成注入。
Effect.provide -
在重载函数上使用无点风格 ——会静默擦除泛型。请使用显式的 lambda:
Effect.map(myOverloadedFn)。Effect.map((x) => myOverloadedFn(x)) -
的 resume 被多次调用 —— resume 必须且只能被调用一次,多次调用会导致未定义行为。
Effect.async -
会静默隐藏错误 —— 它会把类型化的失败转换为无类型的缺陷,请正确处理错误而非使用
orDie。orDie -
对有依赖的服务使用—— merge 不会按顺序构建,如果一个 Layer 需要另一个 Layer 的输出,请使用
Layer.merge。Layer.provide -
与
Fiber.join的差异 —— 极端场景下Fiber.await可能导致终结器提前执行,当资源安全很重要时优先使用join。await -
对无限流调用—— 没有提前调用
runCollect时永远不要这么做,它永远不会结束,并且会消耗无上限的内存。take -
对需要作用域的测试使用—— 需要
it.effect的 Effect 必须使用Scope而非it.scoped,否则会出现类型错误。it.effect
Reference Files
参考文件
Read the relevant reference file when working with a specific concern:
| File | When to read |
|---|---|
| Tagged errors, Cause, defect recovery, error mapping patterns |
| Services, Layers, composition, memoization, provide patterns |
| Fibers, fork variants, Deferred, Semaphore, structured concurrency |
| Scope, acquireRelease, Layer resources, fork + scope interaction |
| Schema definition, transforms, branded types, recursive schemas |
| Stream operators, chunking, backpressure, resourceful streams |
| @effect/vitest, TestClock, Layer mocking, Config mocking |
| HTTP client, FileSystem, Command, runtime, framework integration |
当你处理特定场景的问题时,请阅读对应的参考文件:
| 文件路径 | 适用场景 |
|---|---|
| 带标签的错误、Cause、缺陷恢复、错误映射模式 |
| 服务、Layer、组合、缓存、注入模式 |
| Fibers、fork 变体、Deferred、Semaphore、结构化并发 |
| Scope、acquireRelease、Layer 资源、fork 与 scope 交互 |
| Schema 定义、转换、 branded 类型、递归 Schema |
| Stream 操作符、分块、背压、带资源的 Stream |
| @effect/vitest、TestClock、Layer Mock、Config Mock |
| HTTP 客户端、FileSystem、Command、运行时、框架集成 |