effect-advanced

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Effect 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

Generators read like synchronous code and are strongly preferred over long
.pipe
/
.flatMap
chains for anything beyond trivial composition:
typescript
const program = Effect.gen(function* () {
  const config = yield* ConfigService;
  const user = yield* UserRepo.findById(config.userId);
  return user;
});
Reserve
pipe
for data transformation pipelines and short combinator chains.
生成器的读感与同步代码类似,除了非常简单的组合逻辑之外,都强烈建议优先使用生成器,而非冗长的
.pipe
/
.flatMap
调用链:
typescript
const program = Effect.gen(function* () {
  const config = yield* ConfigService;
  const user = yield* UserRepo.findById(config.userId);
  return user;
});
请将
pipe
保留用于数据转换管道和短组合子调用链。

Never throw — use Effect's error channel

不要主动抛出错误 —— 使用 Effect 的错误通道

Instead of...Use
throw new Error()
Effect.fail(new MyError())
try/catch
on promises
Effect.tryPromise({ try, catch })
Callback APIs
Effect.async((resume) => ...)
Unrecoverable crashes
Effect.die(defect)
不要使用...请使用
throw new Error()
Effect.fail(new MyError())
对 promise 使用
try/catch
Effect.tryPromise({ try, catch })
回调类 API
Effect.async((resume) => ...)
不可恢复的崩溃
Effect.die(defect)

Functions over methods

优先使用函数而非方法

Prefer
Effect.map(e, f)
over
e.pipe(Effect.map(f))
for composability and tree-shaking. Flat imports (
import { Effect } from "effect"
) are fine for applications; namespace imports (
import * as Effect from "effect/Effect"
) are better for libraries.
为了更好的可组合性和 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
已被弃用

Schema has been merged into core
effect
. Import from
"effect"
directly:
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

Effect.runPromise
does not handle
SIGINT
/
SIGTERM
gracefully:
typescript
import { NodeRuntime } from "@effect/platform-node";
NodeRuntime.runMain(program.pipe(Effect.provide(AppLayer)));

Effect.runPromise
无法优雅处理
SIGINT
/
SIGTERM
信号:
typescript
import { NodeRuntime } from "@effect/platform-node";
NodeRuntime.runMain(program.pipe(Effect.provide(AppLayer)));

Error Handling Philosophy

错误处理理念

Failures vs defects — the fundamental distinction

失败 vs 缺陷 —— 核心区别

AspectFailure (expected)Defect (unexpected)
API
Effect.fail(new MyError())
Effect.die(new Error())
Type channelTracked in
E
Never appears in
E
(
never
)
Recovery
catchTag
,
catchAll
,
retry
Only at system boundaries
Rule of thumbYou intend to handle it at call siteBug or impossible state
维度失败(预期内)缺陷(预期外)
对应 API
Effect.fail(new MyError())
Effect.die(new Error())
类型通道
E
中被追踪
永远不会出现在
E
中(类型为
never
恢复方式
catchTag
catchAll
retry
仅能在系统边界处处理
判断标准你打算在调用侧处理它属于 Bug 或者不可能出现的状态

Always use tagged errors

始终使用带标签的错误

Plain
Error
or string failures miss the value of Effect's typed error channel:
typescript
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;
});
普通的
Error
或者字符串类型的错误无法发挥 Effect 类型化错误通道的价值:
typescript
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

catchAll
不会捕获缺陷

This 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
catchAllCause
/
catchAllDefect
at system boundaries (top-level error handlers, HTTP response mappers).

这是最常见的错误处理误区:
typescript
Effect.catchAll(program, handler); // 仅捕获 E 类型的错误 —— 不包含缺陷
Effect.catchAllCause(program, handler); // 捕获所有内容(E + 缺陷 + 中断)
仅在系统边界处(顶层错误处理器、HTTP 响应映射器)使用
catchAllCause
/
catchAllDefect

Dependency 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

Dependencies 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 组合 —— 了解常用操作符

OperationWhenBehavior
Layer.merge(A, B)
Independent servicesBoth build concurrently
Layer.provide(downstream, upstream)
A feeds Bupstream builds first
Layer.fresh(layer)
Force new instanceBypasses memoization
Critical:
Layer.merge
does NOT sequence construction. If B depends on A, use
Layer.provide
, not
Layer.merge
.
操作使用场景行为说明
Layer.merge(A, B)
独立服务两者并发构建
Layer.provide(downstream, upstream)
A 为 B 提供依赖先构建 upstream
Layer.fresh(layer)
强制生成新实例绕过缓存逻辑
重要提示:
Layer.merge
不会按顺序构建。如果 B 依赖 A,请使用
Layer.provide
而非
Layer.merge

One
Effect.provide
at the entry point

在入口处仅调用一次
Effect.provide

Scattered
provide
calls create hidden dependencies and layer duplication:
typescript
// 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);

分散的
provide
调用会产生隐藏依赖和 Layer 重复实例:
typescript
// 错误写法: 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

使用
acquireRelease
时必须搭配
Effect.scoped

Forgetting
Effect.scoped
is the #1 resource management pitfall — resources accumulate until the program exits:
typescript
// 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.scoped
是最常见的资源管理陷阱——资源会一直累积直到程序退出:
typescript
// 错误写法: 作用域永远不会关闭,连接泄漏
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
Exit
value for conditional cleanup.
无论执行成功、失败还是被中断,终结器都会被执行,这是 Effect 保证的。终结器会收到
Exit
值,可以用于实现条件清理逻辑。

Multiple 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

APIUse case
Effect.all([], { concurrency: N })
Bounded parallel execution
Effect.forEach(items, fn, { concurrency: N })
Worker pool pattern
Effect.race(a, b)
First to complete wins, others interrupted
Effect.timeout(e, dur)
Deadline on any effect
Only reach for
Effect.fork
/
Fiber
when high-level APIs are insufficient.
API适用场景
Effect.all([], { concurrency: N })
有界并行执行
Effect.forEach(items, fn, { concurrency: N })
工作池模式
Effect.race(a, b)
取最先完成的结果,其余任务被中断
Effect.timeout(e, dur)
给任意 Effect 设置执行截止时间
仅当高层 API 无法满足需求时,才使用
Effect.fork
/
Fiber

Fork variants — know the lifecycle

Fork 变体 —— 了解生命周期差异

FunctionScopeCleanup
Effect.fork
Parent's scopeAuto-interrupted with parent
Effect.forkDaemon
Global scopeNothing cleans it up — you must
Effect.forkScoped
Nearest ScopeTied to resource lifecycle
Gotcha:
forkDaemon
leaks fibers if you forget to interrupt them.

函数名所属作用域清理逻辑
Effect.fork
父级作用域父级中断时自动被中断
Effect.forkDaemon
全局作用域不会被自动清理 —— 需要开发者手动处理
Effect.forkScoped
最近的作用域与资源生命周期绑定
注意: 如果你忘记手动中断
forkDaemon
创建的 fiber,会导致 fiber 泄漏。

Common Pitfalls

常见陷阱

  1. Floating effects — creating an Effect without yielding or running it is a silent bug.
    Effect.log("msg")
    inside a generator does nothing unless
    yield*
    -ed.
  2. catchAll
    won't catch defects
    — use
    catchAllCause
    at system boundaries for full failure visibility.
  3. Missing
    Effect.scoped
    acquireRelease
    without a scope boundary leaks resources until program exit.
  4. Scattered
    Effect.provide
    — compose all layers and provide once at the entry point.
  5. Point-free on overloaded functions
    Effect.map(myOverloadedFn)
    silently erases generics. Use explicit lambdas:
    Effect.map((x) => myOverloadedFn(x))
    .
  6. Effect.async
    resume called multiple times
    — resume must be called exactly once. Multiple calls cause undefined behavior.
  7. orDie
    silences errors
    — converts typed failures to untyped defects. Handle errors properly instead.
  8. Layer.merge
    for dependent services
    — merge doesn't sequence construction. Use
    Layer.provide
    when one layer needs another's output.
  9. Fiber.join
    vs
    Fiber.await
    join
    can cause premature finalizer execution in edge cases. Prefer
    await
    when resource safety matters.
  10. runCollect
    on infinite streams
    — never call without a prior
    take
    . It will never terminate and consume unbounded memory.
  11. Using
    it.effect
    for scoped tests
    — effects requiring
    Scope
    must use
    it.scoped
    , not
    it.effect
    , or you get a type error.

  1. 悬浮 Effect —— 创建了 Effect 但没有 yield 或者运行它是一种无报错的 Bug。生成器内部的
    Effect.log("msg")
    不会执行,除非你加上
    yield*
  2. catchAll
    不会捕获缺陷
    —— 如需完整的错误可见性,请在系统边界处使用
    catchAllCause
  3. 遗漏
    Effect.scoped
    —— 没有作用域边界的
    acquireRelease
    会导致资源泄漏,直到程序退出才会释放。
  4. 分散的
    Effect.provide
    调用
    —— 组合所有 Layer,在入口处一次性完成注入。
  5. 在重载函数上使用无点风格 ——
    Effect.map(myOverloadedFn)
    会静默擦除泛型。请使用显式的 lambda:
    Effect.map((x) => myOverloadedFn(x))
  6. Effect.async
    的 resume 被多次调用
    —— resume 必须且只能被调用一次,多次调用会导致未定义行为。
  7. orDie
    会静默隐藏错误
    —— 它会把类型化的失败转换为无类型的缺陷,请正确处理错误而非使用
    orDie
  8. 对有依赖的服务使用
    Layer.merge
    —— merge 不会按顺序构建,如果一个 Layer 需要另一个 Layer 的输出,请使用
    Layer.provide
  9. Fiber.join
    Fiber.await
    的差异
    —— 极端场景下
    join
    可能导致终结器提前执行,当资源安全很重要时优先使用
    await
  10. 对无限流调用
    runCollect
    —— 没有提前调用
    take
    时永远不要这么做,它永远不会结束,并且会消耗无上限的内存。
  11. 对需要作用域的测试使用
    it.effect
    —— 需要
    Scope
    的 Effect 必须使用
    it.scoped
    而非
    it.effect
    ,否则会出现类型错误。

Reference Files

参考文件

Read the relevant reference file when working with a specific concern:
FileWhen to read
references/error-handling.md
Tagged errors, Cause, defect recovery, error mapping patterns
references/dependency-injection.md
Services, Layers, composition, memoization, provide patterns
references/concurrency.md
Fibers, fork variants, Deferred, Semaphore, structured concurrency
references/resource-management.md
Scope, acquireRelease, Layer resources, fork + scope interaction
references/schema.md
Schema definition, transforms, branded types, recursive schemas
references/stream.md
Stream operators, chunking, backpressure, resourceful streams
references/testing.md
@effect/vitest, TestClock, Layer mocking, Config mocking
references/platform.md
HTTP client, FileSystem, Command, runtime, framework integration
当你处理特定场景的问题时,请阅读对应的参考文件:
文件路径适用场景
references/error-handling.md
带标签的错误、Cause、缺陷恢复、错误映射模式
references/dependency-injection.md
服务、Layer、组合、缓存、注入模式
references/concurrency.md
Fibers、fork 变体、Deferred、Semaphore、结构化并发
references/resource-management.md
Scope、acquireRelease、Layer 资源、fork 与 scope 交互
references/schema.md
Schema 定义、转换、 branded 类型、递归 Schema
references/stream.md
Stream 操作符、分块、背压、带资源的 Stream
references/testing.md
@effect/vitest、TestClock、Layer Mock、Config Mock
references/platform.md
HTTP 客户端、FileSystem、Command、运行时、框架集成