name: effect-patterns-error-management description: Effect-TS的错误管理模式。在Effect-TS应用中处理错误管理时使用。
Effect-TS 模式:错误管理
这个技能提供了15个精选的Effect-TS错误管理模式。 在处理以下任务时使用此技能:
- 错误管理
- Effect-TS应用中的最佳实践
- 现实世界模式和解决方案
🟢 初级模式
在Option和Either上进行模式匹配
规则: 使用Option.match()和Either.match()对可选值和错误倾向值进行声明式模式匹配
好示例:
基本Option匹配
import { Option } from "effect";
const getUserName = (id: number): Option.Option<string> => {
return id === 1 ? Option.some("Alice") : Option.none();
};
// 使用 .match() 进行声明式模式匹配
const displayUser = (id: number): string =>
getUserName(id).pipe(
Option.match({
onNone: () => "Guest User",
onSome: (name) => `Hello, ${name}!`,
})
);
console.log(displayUser(1)); // "Hello, Alice!"
console.log(displayUser(999)); // "Guest User"
基本Either匹配
import { Either } from "effect";
const validateAge = (age: number): Either.Either<number, string> => {
return age >= 18
? Either.right(age)
: Either.left("Must be 18 or older");
};
// 使用 .match() 进行错误处理
const processAge = (age: number): string =>
validateAge(age).pipe(
Either.match({
onLeft: (error) => `Validation failed: ${error}`,
onRight: (validAge) => `Age ${validAge} is valid`,
})
);
console.log(processAge(25)); // "Age 25 is valid"
console.log(processAge(15)); // "Validation failed: Must be 18 or older"
高级:嵌套匹配
当处理嵌套的Option和Either时,使用嵌套的 .match() 调用:
import { Option, Either } from "effect";
interface UserProfile {
name: string;
age: number;
}
const getUserProfile = (
id: number
): Option.Option<Either.Either<string, UserProfile>> => {
if (id === 0) return Option.none(); // 用户未找到
if (id === 1) return Option.some(Either.left("Profile incomplete"));
return Option.some(Either.right({ name: "Bob", age: 25 }));
};
// 嵌套匹配 - 先在Option上匹配,然后在Either上匹配
const displayProfile = (id: number): string =>
getUserProfile(id).pipe(
Option.match({
onNone: () => "User not found",
onSome: (result) =>
result.pipe(
Either.match({
onLeft: (error) => `Error: ${error}`,
onRight: (profile) => `${profile.name} (${profile.age})`,
})
),
})
);
console.log(displayProfile(0)); // "User not found"
console.log(displayProfile(1)); // "Error: Profile incomplete"
console.log(displayProfile(2)); // "Bob (25)"
反模式:
避免手动条件检查和嵌套三元运算符:
// ❌ 反模式:使用isSome/isLeft进行命令式检查
const name = getUserName(1);
let result: string;
if (Option.isSome(name)) {
result = `Hello, ${name.value}!`;
} else {
result = "Guest User";
}
// ❌ 反模式:嵌套三元运算符
const ageResult = validateAge(25);
const message = ageResult.pipe(
Either.match({
onLeft: () => "Invalid",
onRight: (age) => age >= 21 ? "Can drink" : "Cannot drink",
})
);
// ❌ 反模式:使用链式if-else而不是match
function processValue(value: Option.Option<number>): string {
if (Option.isSome(value)) {
if (value.value > 0) {
return "Positive";
} else if (value.value < 0) {
return "Negative";
} else {
return "Zero";
}
}
return "No value";
}
为什么这些更差:
- 可读性差: 意图隐藏在命令式逻辑中
- 容易出错: 容易忘记情况或引入错误
- 可变状态: 通常需要中间变量
- 可组合性差: 难以管道化和组合操作
理由:
当需要处理 Option 或 Either 值时,使用 .match() 组合子而不是命令式检查。.match() 方法提供了一种声明式、详尽的方式来处理所有情况(Option的Some/None,Either的Right/Left)在一个表达式中。
使用 .match() 当:
- 需要处理成功和失败情况
- 想要类型安全的模式匹配
- 偏好声明式而非命令式代码
- 需要基于情况转换值
.match() 组合子优于手动检查(isSome()、isLeft()),因为:
- 声明式: 清晰表达意图 - “匹配这些情况”
- 类型安全: TypeScript确保所有情况都被处理
- 详尽: 不会意外遗漏情况
- 可组合: 自然地与
.pipe()配合进行链式操作 - 可读: 结构反映数据类型本身
没有 .match(),你需要命令式条件语句,这更难读且更容易出错。
你的第一个错误处理器
规则: 使用catchAll或catchTag从错误中恢复并保持程序运行。
好示例:
import { Effect, Data } from "effect"
// ============================================
// 1. 定义类型化错误
// ============================================
class NetworkError extends Data.TaggedError("NetworkError")<{
readonly url: string
}> {}
class NotFoundError extends Data.TaggedError("NotFoundError")<{
readonly resource: string
}> {}
// ============================================
// 2. 可能失败的函数
// ============================================
const fetchData = (url: string): Effect.Effect<string, NetworkError> =>
url.startsWith("http")
? Effect.succeed(`Data from ${url}`)
: Effect.fail(new NetworkError({ url }))
const findUser = (id: string): Effect.Effect<{ id: string; name: string }, NotFoundError> =>
id === "123"
? Effect.succeed({ id, name: "Alice" })
: Effect.fail(new NotFoundError({ resource: `user:${id}` }))
// ============================================
// 3. 使用catchAll处理所有错误
// ============================================
const withFallback = fetchData("invalid-url").pipe(
Effect.catchAll((error) => {
console.log(`Failed: ${error.url}, using fallback`)
return Effect.succeed("Fallback data")
})
)
// 结果: "Fallback data"
// ============================================
// 4. 使用catchTag处理特定错误
// ============================================
const findUserOrDefault = (id: string) =>
findUser(id).pipe(
Effect.catchTag("NotFoundError", (error) => {
console.log(`User not found: ${error.resource}`)
return Effect.succeed({ id: "guest", name: "Guest User" })
})
)
// ============================================
// 5. 处理多种错误类型
// ============================================
const fetchUser = (url: string, id: string) =>
Effect.gen(function* () {
yield* fetchData(url)
return yield* findUser(id)
})
const robustFetchUser = (url: string, id: string) =>
fetchUser(url, id).pipe(
Effect.catchTags({
NetworkError: (e) => Effect.succeed({ id: "offline", name: `Offline (${e.url})` }),
NotFoundError: (e) => Effect.succeed({ id: "unknown", name: `Unknown (${e.resource})` }),
})
)
// ============================================
// 6. 运行示例
// ============================================
const program = Effect.gen(function* () {
// catchAll示例
const data = yield* withFallback
yield* Effect.log(`Got data: ${data}`)
// catchTag示例
const user = yield* findUserOrDefault("999")
yield* Effect.log(`Got user: ${user.name}`)
// 多种错误类型
const result = yield* robustFetchUser("invalid", "999")
yield* Effect.log(`Robust result: ${result.name}`)
})
Effect.runPromise(program)
理由:
使用 catchAll 处理任何错误,或使用 catchTag 处理特定错误类型。
Effect 在你的类型中使错误显式化:
- 错误是类型化的 - 你确切知道什么可能失败
- 处理或传播 - 不能意外忽略错误
- 恢复选项 - 提供回退、重试或转换
- 没有try/catch - 声明式错误处理
使用match匹配成功和失败
规则: 使用match对Effect、Option或Either的结果进行模式匹配,以声明式方式处理成功和失败情况。
好示例:
import { Effect, Option, Either } from "effect";
// Effect:处理成功和失败
const effect = Effect.fail("Oops!").pipe(
Effect.match({
onFailure: (err) => `Error: ${err}`,
onSuccess: (value) => `Success: ${value}`,
})
); // Effect<string>
// Option:处理Some和None情况
const option = Option.some(42).pipe(
Option.match({
onNone: () => "No value",
onSome: (n) => `Value: ${n}`,
})
); // string
// Either:处理Left和Right情况
const either = Either.left("fail").pipe(
Either.match({
onLeft: (err) => `Error: ${err}`,
onRight: (value) => `Value: ${value}`,
})
); // string
解释:
Effect.match让你在一个地方处理错误和成功通道。Option.match和Either.match让你处理这些类型的所有可能情况,使你的代码详尽且安全。
反模式:
使用嵌套if/else或switch语句检查成功/失败,或忽略可能的错误/none/left情况,这导致代码脆弱且可读性差。
理由:
使用 match 组合子在一个声明式的地方处理成功和失败情况。
这适用于 Effect、Option 和 Either,是健壮、可读错误处理和分支的基础。
使用 match 进行模式匹配使你的代码清晰且类型安全,确保处理所有可能的结果。
它避免了分散的if/else或switch语句,并使你的意图明确。
检查Option和Either情况
规则: 使用isSome、isNone、isLeft和isRight检查Option和Either情况,用于简单、类型安全的条件逻辑。
好示例:
import { Option, Either } from "effect";
// Option:检查值是否为Some或None
const option = Option.some(42);
if (Option.isSome(option)) {
// 这里option.value可用
console.log("We have a value:", option.value);
} else if (Option.isNone(option)) {
console.log("No value present");
}
// Either:检查值是否为Right或Left
const either = Either.left("error");
if (Either.isRight(either)) {
// either.right在这里可用
console.log("Success:", either.right);
} else if (Either.isLeft(either)) {
// either.left在这里可用
console.log("Failure:", either.left);
}
// 过滤Option集合
const options = [Option.some(1), Option.none(), Option.some(3)];
const presentValues = options.filter(Option.isSome).map((o) => o.value); // [1, 3]
解释:
Option.isSome和Option.isNone让你检查存在或缺失。Either.isRight和Either.isLeft让你检查成功或失败。- 这些在过滤或快速条件逻辑中特别有用。
反模式:
手动检查内部标签或属性(例如,option._tag === "Some"),或使用不安全的类型断言,这比使用提供的谓词更不安全且可读性差。
理由:
使用 isSome、isNone、isLeft 和 isRight 谓词检查 Option 或 Either 的情况,用于简单、类型安全的分支。
这些在需要快速检查或基于存在或成功过滤集合时很有用。
这些谓词提供了一种简洁、类型安全的方式来检查你拥有的情况,而无需依赖手动属性检查或不安全的类型断言。
🟡 中级模式
使用catchTag、catchTags和catchAll处理错误
规则: 使用catchTag、catchTags和catchAll处理错误。
好示例:
import { Data, Effect } from "effect";
// 定义领域类型
interface User {
readonly id: string;
readonly name: string;
}
// 定义特定错误类型
class NetworkError extends Data.TaggedError("NetworkError")<{
readonly url: string;
readonly code: number;
}> {}
class ValidationError extends Data.TaggedError("ValidationError")<{
readonly field: string;
readonly message: string;
}> {}
class NotFoundError extends Data.TaggedError("NotFoundError")<{
readonly id: string;
}> {}
// 定义UserService
class UserService extends Effect.Service<UserService>()("UserService", {
sync: () => ({
// 获取用户数据
fetchUser: (
id: string
): Effect.Effect<User, NetworkError | NotFoundError> =>
Effect.gen(function* () {
yield* Effect.logInfo(`Fetching user with id: ${id}`);
if (id === "invalid") {
const url = "/api/users/" + id;
yield* Effect.logWarning(`Network error accessing: ${url}`);
return yield* Effect.fail(new NetworkError({ url, code: 500 }));
}
if (id === "missing") {
yield* Effect.logWarning(`User not found: ${id}`);
return yield* Effect.fail(new NotFoundError({ id }));
}
const user = { id, name: "John Doe" };
yield* Effect.logInfo(`Found user: ${JSON.stringify(user)}`);
return user;
}),
// 验证用户数据
validateUser: (user: User): Effect.Effect<string, ValidationError> =>
Effect.gen(function* () {
yield* Effect.logInfo(`Validating user: ${JSON.stringify(user)}`);
if (user.name.length < 3) {
yield* Effect.logWarning(
`Validation failed: name too short for user ${user.id}`
);
return yield* Effect.fail(
new ValidationError({ field: "name", message: "Name too short" })
);
}
const message = `User ${user.name} is valid`;
yield* Effect.logInfo(message);
return message;
}),
}),
}) {}
// 使用catchTags组合操作进行错误处理
const processUser = (
userId: string
): Effect.Effect<string, never, UserService> =>
Effect.gen(function* () {
const userService = yield* UserService;
yield* Effect.logInfo(`=== Processing user ID: ${userId} ===`);
const result = yield* userService.fetchUser(userId).pipe(
Effect.flatMap(userService.validateUser),
// 使用特定恢复逻辑处理不同错误类型
Effect.catchTags({
NetworkError: (e) =>
Effect.gen(function* () {
const message = `Network error: ${e.code} for ${e.url}`;
yield* Effect.logError(message);
return message;
}),
NotFoundError: (e) =>
Effect.gen(function* () {
const message = `User ${e.id} not found`;
yield* Effect.logWarning(message);
return message;
}),
ValidationError: (e) =>
Effect.gen(function* () {
const message = `Invalid ${e.field}: ${e.message}`;
yield* Effect.logWarning(message);
return message;
}),
})
);
yield* Effect.logInfo(`Result: ${result}`);
return result;
});
// 测试不同场景
const runTests = Effect.gen(function* () {
yield* Effect.logInfo("=== Starting User Processing Tests ===");
const testCases = ["valid", "invalid", "missing"];
const results = yield* Effect.forEach(testCases, (id) => processUser(id));
yield* Effect.logInfo("=== User Processing Tests Complete ===");
return results;
});
// 运行程序
Effect.runPromise(Effect.provide(runTests, UserService.Default));
解释:
使用 catchTag 以类型安全、可组合的方式处理特定错误类型。
反模式:
在你的Effect组合中使用 try/catch 块。它破坏了声明式流程并绕过了Effect强大、类型安全的错误通道。
理由:
为了从失败中恢复,使用 catch* 系列函数。
Effect.catchTag 用于特定标记错误,Effect.catchTags 用于多个错误,Effect.catchAll 用于任何错误。
Effect的结构化错误处理允许你构建弹性应用。
通过使用标记错误和 catchTag,你可以以类型安全的方式用不同逻辑处理不同失败场景。
映射错误以适应你的领域
规则: 使用Effect.mapError转换错误,并在层之间创建干净的架构边界。
好示例:
一个 UserRepository 使用 Database 服务。Database 可能因特定错误失败,但 UserRepository 在将错误暴露给应用其余部分之前,将它们映射到单个通用 RepositoryError。
import { Effect, Data } from "effect";
// 来自数据库层的低级、特定错误
class ConnectionError extends Data.TaggedError("ConnectionError") {}
class QueryError extends Data.TaggedError("QueryError") {}
// 仓库层的通用错误
class RepositoryError extends Data.TaggedError("RepositoryError")<{
readonly cause: unknown;
}> {}
// 内部服务
const dbQuery = (): Effect.Effect<
{ name: string },
ConnectionError | QueryError
> => Effect.fail(new ConnectionError());
// 外部服务使用 `mapError` 创建干净边界。
// 其公共签名仅暴露 `RepositoryError`。
const findUser = (): Effect.Effect<{ name: string }, RepositoryError> =>
dbQuery().pipe(
Effect.mapError((error) => new RepositoryError({ cause: error }))
);
// 演示错误映射
const program = Effect.gen(function* () {
yield* Effect.logInfo("Attempting to find user...");
try {
const user = yield* findUser();
yield* Effect.logInfo(`Found user: ${user.name}`);
} catch (error) {
yield* Effect.logInfo("This won't be reached due to Effect error handling");
}
}).pipe(
Effect.catchAll((error) =>
Effect.gen(function* () {
if (error instanceof RepositoryError) {
yield* Effect.logInfo(`Repository error occurred: ${error._tag}`);
if (
error.cause instanceof ConnectionError ||
error.cause instanceof QueryError
) {
yield* Effect.logInfo(`Original cause: ${error.cause._tag}`);
}
} else {
yield* Effect.logInfo(`Unexpected error: ${error}`);
}
})
)
);
Effect.runPromise(program);
反模式:
允许低级、实现特定的错误"泄漏"出服务的公共API。这在层之间创建了紧密耦合。
import { Effect } from "effect";
import { ConnectionError, QueryError } from "./somewhere"; // 来自先前示例
// ❌ 错误:此函数的错误通道是"泄漏的"。
// 它暴露了数据库的内部实现细节。
const findUserUnsafely = (): Effect.Effect<
{ name: string },
ConnectionError | QueryError // <-- 泄漏抽象
> => {
// ... 调用数据库的逻辑
return Effect.fail(new ConnectionError());
};
// 现在,任何调用 `findUserUnsafely` 的代码都必须知道并处理
// `ConnectionError` 和 `QueryError`。如果我们更改数据库,
// 所有调用代码可能都不得不更改。
理由:
当内部服务可能因特定错误失败时,在外部服务中使用 Effect.mapError 捕获这些特定错误,并将它们转换为适合其自身领域的更通用错误。
此模式对于创建干净的架构边界和防止"泄漏抽象"至关重要。你的应用的外部层(例如,UserService)不应暴露其依赖层(例如,可能因 ConnectionError 或 QueryError 失败的 Database)的内部失败细节。
通过使用 Effect.mapError,外部层可以定义自己的更抽象的错误类型(如 RepositoryError),并将所有特定的低级错误映射到其中。这解耦了层。如果你后来交换数据库实现,只需更新仓库层内的映射逻辑;使用仓库的代码无需更改。
使用Schedule控制重复
规则: 使用Schedule创建可组合策略,控制效果的重复和重试。
好示例:
此示例通过创建常见的健重重试策略来演示组合:具有抖动的指数退避,限制为5次尝试。
import { Effect, Schedule, Duration } from "effect";
// 一个可能失败的简单效果
const flakyEffect = Effect.try({
try: () => {
if (Math.random() > 0.2) {
throw new Error("Transient error");
}
return "Operation succeeded!";
},
catch: (error: unknown) => {
Effect.logInfo("Operation failed, retrying...");
return error;
},
});
// --- 构建可组合的Schedule ---
// 1. 从基本指数退避开始(100ms, 200ms, 400ms...)
const exponentialBackoff = Schedule.exponential("100 millis");
// 2. 添加随机抖动以避免惊群问题
const withJitter = Schedule.jittered(exponentialBackoff);
// 3. 将计划限制为最多5次重复
const limitedWithJitter = Schedule.compose(withJitter, Schedule.recurs(5));
// --- 使用Schedule ---
const program = Effect.gen(function* () {
yield* Effect.logInfo("Starting operation...");
const result = yield* Effect.retry(flakyEffect, limitedWithJitter);
yield* Effect.logInfo(`Final result: ${result}`);
});
// 运行程序
Effect.runPromise(program);
反模式:
编写手动、命令式的重试逻辑。这冗长、有状态、难以推理且不易组合。
import { Effect } from "effect";
import { flakyEffect } from "./somewhere";
// ❌ 错误:手动、有状态且复杂的重试逻辑。
function manualRetry(
effect: typeof flakyEffect,
retriesLeft: number,
delay: number
): Effect.Effect<string, "ApiError"> {
return effect.pipe(
Effect.catchTag("ApiError", () => {
if (retriesLeft > 0) {
return Effect.sleep(delay).pipe(
Effect.flatMap(() => manualRetry(effect, retriesLeft - 1, delay * 2))
);
}
return Effect.fail("ApiError" as const);
})
);
}
const program = manualRetry(flakyEffect, 5, 100);
理由:
一个 Schedule<In, Out> 是一个高度可组合的蓝图,定义了重复计划。它接受类型为 In 的输入(例如,来自失败效果的错误)并产生类型为 Out 的输出(例如,继续的决定)。使用 Schedule 与 Effect.repeat 和 Effect.retry 等操作符来控制复杂的重复逻辑。
虽然你可以编写手动循环或递归函数,但 Schedule 提供了一种更强大、声明式和可组合的方式来管理重复。关键好处是:
- 声明式: 你将_什么_(要运行的效果)与_如何_和_何时_(它运行的计划)分开。
- 可组合: 你可以从简单的原始计划构建复杂计划。例如,你可以创建一个计划,运行"最多5次,具有指数退避,加上一些随机抖动",通过组合
Schedule.recurs、Schedule.exponential和Schedule.jittered。 - 有状态: 一个
Schedule跟踪自己的状态(如重复次数),使得创建依赖于执行历史的策略变得容易。
利用Effect的内置结构化日志
规则: 利用Effect的内置结构化日志。
好示例:
import { Effect } from "effect";
const program = Effect.logDebug("Processing user", { userId: 123 });
// 启用调试日志运行程序
Effect.runSync(
program.pipe(Effect.tap(() => Effect.log("Debug logging enabled")))
);
解释:
使用Effect的日志系统确保你的日志是结构化、可过滤和上下文感知的。
反模式:
在Effect组合中直接调用 console.log。这是一个未管理的副作用,绕过了Effect日志系统的所有好处。
理由:
使用内置的 Effect.log* 系列函数进行所有应用日志记录,而不是使用 console.log。
Effect的日志器是结构化、上下文感知(带有跟踪ID)、可通过 Layer 配置且可测试的。它是一个一等公民,而不是未管理的副作用。
使用matchTag和matchTags匹配标记联合
规则: 使用matchTag和matchTags以声明式、类型安全的方式处理标记联合或自定义错误类型的特定情况。
好示例:
import { Data, Effect } from "effect";
// 定义标记错误类型
class NotFoundError extends Data.TaggedError("NotFoundError")<{}> {}
class ValidationError extends Data.TaggedError("ValidationError")<{
message: string;
}> {}
type MyError = NotFoundError | ValidationError;
// Effect:匹配特定错误标签
const effect: Effect.Effect<string, never, never> = Effect.fail(
new ValidationError({ message: "Invalid input" }) as MyError
).pipe(
Effect.catchTags({
NotFoundError: () => Effect.succeed("Not found!"),
ValidationError: (err) =>
Effect.succeed(`Validation failed: ${err.message}`),
})
); // Effect<string>
解释:
matchTag让你在标记联合或自定义错误类型的特定标签上分支。- 这比使用
instanceof或手动属性检查更安全且更可维护。
反模式:
使用 instanceof、手动属性检查或switch语句来区分情况,这比声明式模式匹配更容易出错且类型安全性差。
理由:
使用 matchTag 和 matchTags 组合子对标记联合或自定义错误类型的特定情况进行模式匹配。
这使得精确、类型安全的分支成为可能,特别适用于处理领域特定错误或ADT。
标记联合(又名代数数据类型或ADT)是建模领域逻辑的强大方式。
在标签上进行模式匹配让你显式处理每个情况,使你的代码健壮、可维护且详尽。
条件分支工作流
规则: 使用基于谓词的操作符如Effect.filter和Effect.if以声明式控制工作流分支。
好示例:
这里,我们使用带有命名谓词的 Effect.filterOrFail 在继续之前验证用户。意图非常清晰,业务规则(isActive、isAdmin)是可重用的。
import { Effect } from "effect";
interface User {
id: number;
status: "active" | "inactive";
roles: string[];
}
type UserError = "DbError" | "UserIsInactive" | "UserIsNotAdmin";
const findUser = (id: number): Effect.Effect<User, "DbError"> =>
Effect.succeed({ id, status: "active", roles: ["admin"] });
// 可重用、可测试的谓词,记录业务规则。
const isActive = (user: User): boolean => user.status === "active";
const isAdmin = (user: User): boolean => user.roles.includes("admin");
const program = (id: number): Effect.Effect<string, UserError> =>
findUser(id).pipe(
// 使用Effect.filterOrFail验证用户是否活跃
Effect.filterOrFail(isActive, () => "UserIsInactive" as const),
// 使用Effect.filterOrFail验证用户是否是管理员
Effect.filterOrFail(isAdmin, () => "UserIsNotAdmin" as const),
// 成功情况
Effect.map((user) => `Welcome, admin user #${user.id}!`)
);
// 然后我们可以以类型安全的方式处理特定失败。
const handled = program(123).pipe(
Effect.match({
onFailure: (error) => {
switch (error) {
case "UserIsNotAdmin":
return "Access denied: requires admin role.";
case "UserIsInactive":
return "Access denied: user is not active.";
case "DbError":
return "Error: could not find user.";
default:
return `Unknown error: ${error}`;
}
},
onSuccess: (result) => result,
})
);
// 运行程序
const programWithLogging = Effect.gen(function* () {
const result = yield* handled;
yield* Effect.log(result);
return result;
});
Effect.runPromise(programWithLogging);
反模式:
使用 Effect.flatMap 和手动 if 语句,并忘记处理 else 情况。这是一个常见错误,导致推断类型为 Effect<void, ...>,这可能在后续引起混淆的类型错误,因为成功值丢失。
import { Effect } from "effect";
import { findUser, isAdmin } from "./somewhere"; // 来自先前示例
// ❌ 错误:缺少 `else` 情况。
const program = (id: number) =>
findUser(id).pipe(
Effect.flatMap((user) => {
if (isAdmin(user)) {
// 这返回Effect<User>,但如果用户不是管理员会发生什么?
return Effect.succeed(user);
}
// 因为没有 `else` 分支,TypeScript推断此
// 块也可以隐式返回 `void`。
// 结果类型是Effect<User | void, "DbError">,这有问题。
}),
// 这个 `map` 现在会有类型错误,因为 `u` 可能是 `void`。
Effect.map((u) => `Welcome, ${u.name}!`)
);
// `Effect.filterOrFail` 完全避免了此问题,通过强制失败,
// 从而保持成功通道清洁并正确类型化。
为什么这更好
- 这是一个真正的错误: 这不仅是风格问题;它是一个合理的逻辑错误,导致类型不正确和代码损坏。
- 这是一个常见错误: 新接触函数式管道的开发者经常忘记每条路径必须返回值。
- 它强化了"为什么": 它完美地演示了_为什么_
Effect.filterOrFail更优:filterOrFail保证如果条件失败,计算失败,保持成功通道的完整性。
理由:
为了在 Effect 管道中基于成功值做决策,使用基于谓词的操作符:
- 验证并失败: 使用
Effect.filterOrFail(predicate, onFailure)在条件不满足时停止工作流。 - 选择路径: 使用
Effect.if(condition, { onTrue, onFalse })或Effect.gen基于条件执行不同效果。
此模式允许你将决策逻辑直接嵌入到组合管道中,使你的代码更声明式和可读。它解决了两个关键问题:
- 关注点分离: 它干净地将产生值的逻辑与验证或对该值做决策的逻辑分开。
- 可重用业务逻辑: 一个谓词函数(例如,
const isAdmin = (user: User) => ...)成为一个命名的、可重用的、可测试的业务逻辑片段,远优于在整个代码中散布内联if语句。
使用这些操作符将条件逻辑转换为 Effect 的可组合部分,而不是破坏流程的命令式语句。
使用matchEffect进行效果式模式匹配
规则: 使用matchEffect对Effect的结果进行模式匹配,为成功和失败情况运行效果式逻辑。
好示例:
import { Effect } from "effect";
// Effect:在成功或失败时运行不同效果
const effect = Effect.fail("Oops!").pipe(
Effect.matchEffect({
onFailure: (err) => Effect.logError(`Error: ${err}`),
onSuccess: (value) => Effect.log(`Success: ${value}`),
})
); // Effect<void>
解释:
matchEffect允许你为成功和失败情况运行Effect。- 这对于日志记录、清理、重试或任何依赖于结果的效果式副作用很有用。
反模式:
使用 match 返回值,然后将它们包装在Effect中,或为副作用重复逻辑,而不是使用 matchEffect 进行直接效果式分支。
理由:
使用 matchEffect 组合子基于Effect是否成功或失败执行效果式分支。
这允许你为每种情况运行不同效果,实现丰富、可组合的工作流。
有时,处理成功或失败需要运行额外的效果(例如,日志记录、重试、清理)。
matchEffect 让你以声明式方式做到这一点,保持代码可组合且类型安全。
基于特定错误重试操作
规则: 使用基于谓词的重试策略,仅对特定、可恢复的错误重试操作。
好示例:
此示例模拟了一个可能因不同、特定错误类型失败的API客户端。重试策略配置为_仅_在 ServerBusyError 上重试,并在 NotFoundError 上立即放弃。
import { Data, Effect, Schedule } from "effect";
// 为我们的API客户端定义特定的标记错误
class ServerBusyError extends Data.TaggedError("ServerBusyError") {}
class NotFoundError extends Data.TaggedError("NotFoundError") {}
let attemptCount = 0;
// 可能以不同方式失败的脆弱API调用
const flakyApiCall = Effect.try({
try: () => {
attemptCount++;
const random = Math.random();
if (attemptCount <= 2) {
// 前两次尝试因ServerBusyError失败(可重试)
console.log(
`Attempt ${attemptCount}: API call failed - Server is busy. Retrying...`
);
throw new ServerBusyError();
}
// 第三次尝试成功
console.log(`Attempt ${attemptCount}: API call succeeded!`);
return { data: "success", attempt: attemptCount };
},
catch: (e) => e as ServerBusyError | NotFoundError,
});
// 一个谓词,仅对我们要重试的错误返回true
const isRetryableError = (e: ServerBusyError | NotFoundError) =>
e._tag === "ServerBusyError";
// 一个策略,重试3次,但仅当错误是可重试的
const selectiveRetryPolicy = Schedule.recurs(3).pipe(
Schedule.whileInput(isRetryableError),
Schedule.addDelay(() => "100 millis")
);
const program = Effect.gen(function* () {
yield* Effect.logInfo("=== Retry Based on Specific Errors Demo ===");
try {
const result = yield* flakyApiCall.pipe(Effect.retry(selectiveRetryPolicy));
yield* Effect.logInfo(`Success: ${JSON.stringify(result)}`);
return result;
} catch (error) {
yield* Effect.logInfo("This won't be reached due to Effect error handling");
return null;
}
}).pipe(
Effect.catchAll((error) =>
Effect.gen(function* () {
if (error instanceof NotFoundError) {
yield* Effect.logInfo("Failed with NotFoundError - not retrying");
} else if (error instanceof ServerBusyError) {
yield* Effect.logInfo("Failed with ServerBusyError after all retries");
} else {
yield* Effect.logInfo(`Failed with unexpected error: ${error}`);
}
return null;
})
)
);
// 同时演示NotFoundError未被重试的情况
const demonstrateNotFound = Effect.gen(function* () {
yield* Effect.logInfo("
=== Demonstrating Non-Retryable Error ===");
const alwaysNotFound = Effect.fail(new NotFoundError());
const result = yield* alwaysNotFound.pipe(
Effect.retry(selectiveRetryPolicy),
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.logInfo(`NotFoundError was not retried: ${error._tag}`);
return null;
})
)
);
return result;
});
Effect.runPromise(program.pipe(Effect.flatMap(() => demonstrateNotFound)));
反模式:
使用在所有错误上重试的通用 Effect.retry。这可能导致资源浪费并掩盖永久问题。
import { Effect, Schedule } from "effect";
import { flakyApiCall } from "./somewhere"; // 来自先前示例
// ❌ 错误:此策略将重试,即使API返回404 Not Found。
// 这浪费时间和网络请求在一个永远不会成功的错误上。
const blindRetryPolicy = Schedule.recurs(3);
const program = flakyApiCall.pipe(Effect.retry(blindRetryPolicy));
理由:
为了选择性重试操作,使用带有谓词的 Effect.retry。最常见的方式是使用 Schedule.whileInput((error) => ...),只要谓词对发生的错误返回 true,就会继续重试。
并非所有错误都是平等的。在永久错误如"权限被拒绝"或"未找到"上重试是无意义的,并可能隐藏潜在问题。你只应在_瞬态_、可恢复的错误上重试,例如网络超时或"服务器繁忙"响应。
通过向重试计划添加谓词,你获得对重试逻辑的细粒度控制。这允许你构建更智能、更高效的错误处理系统,适当响应不同的失败模式。这是构建外部API健壮客户端的常见要求。
使用catchTag和catchTags处理特定错误
规则: 使用catchTag和catchTags处理Effect失败通道中的特定标记错误类型,提供针对性的恢复逻辑。
好示例:
import { Effect, Data } from "effect";
// 定义标记错误类型
class NotFoundError extends Data.TaggedError("NotFoundError")<{}> {}
class ValidationError extends Data.TaggedError("ValidationError")<{
message: string;
}> {}
type MyError = NotFoundError | ValidationError;
// Effect:仅处理ValidationError,让其他传播
const effect = Effect.fail(
new ValidationError({ message: "Invalid input" }) as MyError
).pipe(
Effect.catchTag("ValidationError", (err) =>
Effect.succeed(`Recovered from validation error: ${err.message}`)
)
); // Effect<string>
// Effect:处理多个错误标签
const effect2 = Effect.fail(new NotFoundError() as MyError).pipe(
Effect.catchTags({
NotFoundError: () => Effect.succeed("Handled not found!"),
ValidationError: (err) =>
Effect.succeed(`Handled validation: ${err.message}`),
})
); // Effect<string>
解释:
catchTag让你从特定标记错误类型恢复。catchTags让你在一个地方处理多个标记错误类型。- 未处理的错误继续传播,保持错误安全。
反模式:
通用地捕获所有错误(例如,使用 catchAll)并使用手动类型检查或属性检查,这比使用标记错误组合子更不安全且更容易出错。
理由:
使用 catchTag 和 catchTags 组合子从Effect失败通道中的特定标记错误类型恢复或处理。
这使得精确、类型安全的错误恢复成为可能,特别适用于领域特定错误处理。
并非所有错误应以相同方式处理。
通过在特定错误标签上匹配,你可以为每种错误类型提供针对性的恢复逻辑,同时根据需要让未处理的错误传播。
使用重试和超时处理脆弱操作
规则: 使用Effect.retry和Effect.timeout构建对缓慢或间歇性失败效果的弹性。
好示例:
此程序尝试从脆弱API获取数据。如果失败,它将重试请求最多3次,每次延迟增加。如果任何单次尝试耗时超过2秒,它也会完全放弃。
import { Data, Duration, Effect, Schedule } from "effect";
// 定义领域类型
interface ApiResponse {
readonly data: string;
}
// 定义错误类型
class ApiError extends Data.TaggedError("ApiError")<{
readonly message: string;
readonly attempt: number;
}> {}
class TimeoutError extends Data.TaggedError("TimeoutError")<{
readonly duration: string;
readonly attempt: number;
}> {}
// 定义API服务
class ApiService extends Effect.Service<ApiService>()("ApiService", {
sync: () => ({
// 可能失败或缓慢的脆弱API调用
fetchData: (): Effect.Effect<ApiResponse, ApiError | TimeoutError> =>
Effect.gen(function* () {
const attempt = Math.floor(Math.random() * 5) + 1;
yield* Effect.logInfo(`Attempt ${attempt}: Making API call...`);
if (Math.random() > 0.3) {
yield* Effect.logWarning(`Attempt ${attempt}: API call failed`);
return yield* Effect.fail(
new ApiError({
message: "API Error",
attempt,
})
);
}
const delay = Math.random() * 3000;
yield* Effect.logInfo(
`Attempt ${attempt}: API call will take ${delay.toFixed(0)}ms`
);
yield* Effect.sleep(Duration.millis(delay));
const response = { data: "some important data" };
yield* Effect.logInfo(
`Attempt ${attempt}: API call succeeded with data: ${JSON.stringify(response)}`
);
return response;
}),
}),
}) {}
// 定义重试策略:指数退避,最多3次重试
const retryPolicy = Schedule.exponential(Duration.millis(100)).pipe(
Schedule.compose(Schedule.recurs(3)),
Schedule.tapInput((error: ApiError | TimeoutError) =>
Effect.logWarning(
`Retrying after error: ${error._tag} (Attempt ${error.attempt})`
)
)
);
// 创建具有适当错误处理的程序
const program = Effect.gen(function* () {
const api = yield* ApiService;
yield* Effect.logInfo("=== Starting API calls with retry and timeout ===");
// 进行多次测试调用
for (let i = 1; i <= 3; i++) {
yield* Effect.logInfo(`
--- Test Call ${i} ---`);
const result = yield* api.fetchData().pipe(
Effect.timeout(Duration.seconds(2)),
Effect.catchTag("TimeoutException", () =>
Effect.fail(new TimeoutError({ duration: "2 seconds", attempt: i }))
),
Effect.retry(retryPolicy),
Effect.catchTags({
ApiError: (error) =>
Effect.gen(function* () {
yield* Effect.logError(
`All retries failed: ${error.message} (Last attempt: ${error.attempt})`
);
return { data: "fallback data due to API error" } as ApiResponse;
}),
TimeoutError: (error) =>
Effect.gen(function* () {
yield* Effect.logError(
`All retries timed out after ${error.duration} (Last attempt: ${error.attempt})`
);
return { data: "fallback data due to timeout" } as ApiResponse;
}),
})
);
yield* Effect.logInfo(`Result: ${JSON.stringify(result)}`);
}
yield* Effect.logInfo("
=== API calls complete ===");
});
// 运行程序
Effect.runPromise(Effect.provide(program, ApiService.Default));
反模式:
编写手动重试和超时逻辑。这冗长、复杂且容易出错。它用Effect可以声明式处理的关注点混乱了你的业务逻辑。
// ❌ 错误:手动、复杂且容易出错的逻辑。
async function manualRetryAndTimeout() {
for (let i = 0; i < 3; i++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 2000);
const response = await fetch("...", { signal: controller.signal });
clearTimeout(timeoutId);
return await response.json();
} catch (error) {
if (i === 2) throw error; // 最后一次尝试,重新抛出
await new Promise((res) => setTimeout(res, 100 * 2 ** i)); // 手动退避
}
}
}
理由:
为了构建能够承受不可靠外部系统的健壮应用,将两个关键操作符应用于你的效果:
Effect.retry(policy): 根据计划自动重新运行失败效果。Effect.timeout(duration): 中断耗时太长的效果。
在分布式系统中,失败是正常的。API可能间歇性失败,网络延迟可能激增。硬编码你的应用仅尝试一次操作使其脆弱。
-
重试:
Effect.retry操作符与Schedule策略结合,提供了一种强大、声明式的方式处理瞬态失败。无需编写复杂的try/catch循环,你可以简单定义一个策略,如"重试3次,尝试之间具有指数退避延迟。" -
超时: 一个操作可能不会失败,而是无限期挂起。
Effect.timeout通过让你的效果与计时器竞赛来防止这种情况。如果你的效果在指定持续时间内未完成,它会自动中断,防止你的应用卡住。
结合这两个模式是与任何外部服务交互的最佳实践。
🟠 高级模式
通过检查Cause处理意外错误
规则: 通过检查Cause处理意外错误。
好示例:
import { Cause, Effect, Data, Schedule, Duration } from "effect";
// 定义领域类型
interface DatabaseConfig {
readonly url: string;
}
interface DatabaseConnection {
readonly success: true;
}
interface UserData {
readonly id: string;
readonly name: string;
}
// 定义错误类型
class DatabaseError extends Data.TaggedError("DatabaseError")<{
readonly operation: string;
readonly details: string;
}> {}
class ValidationError extends Data.TaggedError("ValidationError")<{
readonly field: string;
readonly message: string;
}> {}
// 定义数据库服务
class DatabaseService extends Effect.Service<DatabaseService>()(
"DatabaseService",
{
sync: () => ({
// 具有适当错误处理的数据库连接
connect: (
config: DatabaseConfig
): Effect.Effect<DatabaseConnection, DatabaseError> =>
Effect.gen(function* () {
yield* Effect.logInfo(`Connecting to database: ${config.url}`);
if (!config.url) {
const error = new DatabaseError({
operation: "connect",
details: "Missing URL",
});
yield* Effect.logError(`Database error: ${JSON.stringify(error)}`);
return yield* Effect.fail(error);
}
// 模拟意外错误
if (config.url === "invalid") {
yield* Effect.logError("Invalid connection string");
return yield* Effect.sync(() => {
throw new Error("Failed to parse connection string");
});
}
if (config.url === "timeout") {
yield* Effect.logError("Connection timeout");
return yield* Effect.sync(() => {
throw new Error("Connection timed out");
});
}
yield* Effect.logInfo("Database connection successful");
return { success: true };
}),
}),
}
) {}
// 定义用户服务
class UserService extends Effect.Service<UserService>()("UserService", {
sync: () => ({
// 验证解析用户数据
parseUser: (input: unknown): Effect.Effect<UserData, ValidationError> =>
Effect.gen(function* () {
yield* Effect.logInfo(`Parsing user data: ${JSON.stringify(input)}`);
try {
if (typeof input !== "object" || !input) {
const error = new ValidationError({
field: "input",
message: "Invalid input type",
});
yield* Effect.logWarning(
`Validation error: ${JSON.stringify(error)}`
);
throw error;
}
const data = input as Record<string, unknown>;
if (typeof data.id !== "string" || typeof data.name !== "string") {
const error = new ValidationError({
field: "input",
message: "Missing required fields",
});
yield* Effect.logWarning(
`Validation error: ${JSON.stringify(error)}`
);
throw error;
}
const user = { id: data.id, name: data.name };
yield* Effect.logInfo(
`Successfully parsed user: ${JSON.stringify(user)}`
);
return user;
} catch (e) {
if (e instanceof ValidationError) {
return yield* Effect.fail(e);
}
yield* Effect.logError(
`Unexpected error: ${e instanceof Error ? e.message : String(e)}`
);
throw e;
}
}),
}),
}) {}
// 定义测试服务
class TestService extends Effect.Service<TestService>()("TestService", {
sync: () => {
// 创建实例方法
const printCause = (
prefix: string,
cause: Cause.Cause<unknown>
): Effect.Effect<void, never, never> =>
Effect.gen(function* () {
yield* Effect.logInfo(`
=== ${prefix} ===`);
if (Cause.isDie(cause)) {
const defect = Cause.failureOption(cause);
if (defect._tag === "Some") {
const error = defect.value as Error;
yield* Effect.logError("Defect (unexpected error)");
yield* Effect.logError(`Message: ${error.message}`);
yield* Effect.logError(
`Stack: ${error.stack?.split("
")[1]?.trim() ?? "N/A"}`
);
}
} else if (Cause.isFailure(cause)) {
const error = Cause.failureOption(cause);
yield* Effect.logWarning("Expected failure");
yield* Effect.logWarning(`Error: ${JSON.stringify(error)}`);
}
// 在Effect.gen内不要返回Effect,直接返回值
return void 0;
});
const runScenario = <E, A extends { [key: string]: any }>(
name: string,
program: Effect.Effect<A, E>
): Effect.Effect<void, never, never> =>
Effect.gen(function* () {
yield* Effect.logInfo(`
=== Testing: ${name} ===`);
type TestError = {
readonly _tag: "error";
readonly cause: Cause.Cause<E>;
};
const result = yield* Effect.catchAllCause(program, (cause) =>
Effect.succeed({ _tag: "error" as const, cause } as TestError)
);
if ("cause" in result) {
yield* printCause("Error details", result.cause);
} else {
yield* Effect.logInfo(`Success: ${JSON.stringify(result)}`);
}
// 在Effect.gen内不要返回Effect,直接返回值
return void 0;
});
// 返回绑定方法
return {
printCause,
runScenario,
};
},
}) {}
// 创建具有适当错误处理的程序
const program = Effect.gen(function* () {
const db = yield* DatabaseService;
const users = yield* UserService;
const test = yield* TestService;
yield* Effect.logInfo("=== Starting Error Handling Tests ===");
// 测试预期的数据库错误
yield* test.runScenario(
"Expected database error",
Effect.gen(function* () {
const result = yield* Effect.retry(
db.connect({ url: "" }),
Schedule.exponential(100)
).pipe(
Effect.timeout(Duration.seconds(5)),
Effect.catchAll(() => Effect.fail("Connection timeout"))
);
return result;
})
);
// 测试意外的连接错误
yield* test.runScenario(
"Unexpected connection error",
Effect.gen(function* () {
const result = yield* Effect.retry(
db.connect({ url: "invalid" }),
Schedule.recurs(3)
).pipe(
Effect.catchAllCause((cause) =>
Effect.gen(function* () {
yield* Effect.logError("Failed after 3 retries");
yield* Effect.logError(Cause.pretty(cause));
return yield* Effect.fail("Max retries exceeded");
})
)
);
return result;
})
);
// 测试具有恢复的用户验证
yield* test.runScenario(
"Valid user data",
Effect.gen(function* () {
const result = yield* users
.parseUser({ id: "1", name: "John" })
.pipe(
Effect.orElse(() =>
Effect.succeed({ id: "default", name: "Default User" })
)
);
return result;
})
);
// 测试具有超时的并发错误处理
yield* test.runScenario(
"Concurrent operations",
Effect.gen(function* () {
const results = yield* Effect.all(
[
db.connect({ url: "" }).pipe(
Effect.timeout(Duration.seconds(1)),
Effect.catchAll(() => Effect.succeed({ success: true }))
),
users.parseUser({ id: "invalid" }).pipe(
Effect.timeout(Duration.seconds(1)),
Effect.catchAll(() =>
Effect.succeed({ id: "timeout", name: "Timeout" })
)
),
],
{ concurrency: 2 }
);
return results;
})
);
yield* Effect.logInfo("
=== Error Handling Tests Complete ===");
// 在Effect.gen内不要返回Effect,直接返回值
return void 0;
});
// 运行具有所有服务的程序
Effect.runPromise(
Effect.provide(
Effect.provide(
Effect.provide(program, TestService.Default),
DatabaseService.Default
),
UserService.Default
)
);
解释:
通过检查 Cause,你可以区分预期和意外失败,适当记录或升级。
反模式:
使用简单的 Effect.catchAll 可能危险地将预期错误和意外缺陷混为一谈,将关键错误掩盖为可恢复错误。
理由:
为了构建真正弹性的应用,区分已知业务错误(Fail)和未知缺陷(Die)。使用 Effect.catchAllCause 检查失败的完整 Cause。
Cause 对象解释_为什么_效果失败。Fail 是预期错误(例如,ValidationError)。Die 是意外缺陷(例如,抛出的异常)。它们应被不同地处理。