Effect-TS错误管理模式Skill effect-patterns-error-management

这个技能专注于Effect-TS库中的错误管理,提供了15个精选模式,帮助开发者在TypeScript应用中实现健壮的错误处理、遵循最佳实践并应用现实世界解决方案。适用于处理错误管理、函数式编程和构建可靠应用,关键词:Effect-TS, 错误处理, TypeScript, 函数式编程, 设计模式

后端开发 0 次安装 0 次浏览 更新于 3/8/2026

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";
}

为什么这些更差:

  • 可读性差: 意图隐藏在命令式逻辑中
  • 容易出错: 容易忘记情况或引入错误
  • 可变状态: 通常需要中间变量
  • 可组合性差: 难以管道化和组合操作

理由:

当需要处理 OptionEither 值时,使用 .match() 组合子而不是命令式检查。.match() 方法提供了一种声明式、详尽的方式来处理所有情况(Option的Some/None,Either的Right/Left)在一个表达式中。

使用 .match() 当:

  • 需要处理成功和失败情况
  • 想要类型安全的模式匹配
  • 偏好声明式而非命令式代码
  • 需要基于情况转换值

.match() 组合子优于手动检查(isSome()isLeft()),因为:

  1. 声明式: 清晰表达意图 - “匹配这些情况”
  2. 类型安全: TypeScript确保所有情况都被处理
  3. 详尽: 不会意外遗漏情况
  4. 可组合: 自然地与 .pipe() 配合进行链式操作
  5. 可读: 结构反映数据类型本身

没有 .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 在你的类型中使错误显式化:

  1. 错误是类型化的 - 你确切知道什么可能失败
  2. 处理或传播 - 不能意外忽略错误
  3. 恢复选项 - 提供回退、重试或转换
  4. 没有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.matchEither.match 让你处理这些类型的所有可能情况,使你的代码详尽且安全。

反模式:

使用嵌套if/else或switch语句检查成功/失败,或忽略可能的错误/none/left情况,这导致代码脆弱且可读性差。

理由:

使用 match 组合子在一个声明式的地方处理成功和失败情况。
这适用于 EffectOptionEither,是健壮、可读错误处理和分支的基础。

使用 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.isSomeOption.isNone 让你检查存在或缺失。
  • Either.isRightEither.isLeft 让你检查成功或失败。
  • 这些在过滤或快速条件逻辑中特别有用。

反模式:

手动检查内部标签或属性(例如,option._tag === "Some"),或使用不安全的类型断言,这比使用提供的谓词更不安全且可读性差。

理由:

使用 isSomeisNoneisLeftisRight 谓词检查 OptionEither 的情况,用于简单、类型安全的分支。
这些在需要快速检查或基于存在或成功过滤集合时很有用。

这些谓词提供了一种简洁、类型安全的方式来检查你拥有的情况,而无需依赖手动属性检查或不安全的类型断言。


🟡 中级模式

使用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)不应暴露其依赖层(例如,可能因 ConnectionErrorQueryError 失败的 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 的输出(例如,继续的决定)。使用 ScheduleEffect.repeatEffect.retry 等操作符来控制复杂的重复逻辑。


虽然你可以编写手动循环或递归函数,但 Schedule 提供了一种更强大、声明式和可组合的方式来管理重复。关键好处是:

  • 声明式: 你将_什么_(要运行的效果)与_如何_和_何时_(它运行的计划)分开。
  • 可组合: 你可以从简单的原始计划构建复杂计划。例如,你可以创建一个计划,运行"最多5次,具有指数退避,加上一些随机抖动",通过组合 Schedule.recursSchedule.exponentialSchedule.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语句来区分情况,这比声明式模式匹配更容易出错且类型安全性差。

理由:

使用 matchTagmatchTags 组合子对标记联合或自定义错误类型的特定情况进行模式匹配。
这使得精确、类型安全的分支成为可能,特别适用于处理领域特定错误或ADT。

标记联合(又名代数数据类型或ADT)是建模领域逻辑的强大方式。
在标签上进行模式匹配让你显式处理每个情况,使你的代码健壮、可维护且详尽。


条件分支工作流

规则: 使用基于谓词的操作符如Effect.filter和Effect.if以声明式控制工作流分支。

好示例:

这里,我们使用带有命名谓词的 Effect.filterOrFail 在继续之前验证用户。意图非常清晰,业务规则(isActiveisAdmin)是可重用的。

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 基于条件执行不同效果。

此模式允许你将决策逻辑直接嵌入到组合管道中,使你的代码更声明式和可读。它解决了两个关键问题:

  1. 关注点分离: 它干净地将产生值的逻辑与验证或对该值做决策的逻辑分开。
  2. 可重用业务逻辑: 一个谓词函数(例如,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)并使用手动类型检查或属性检查,这比使用标记错误组合子更不安全且更容易出错。

理由:

使用 catchTagcatchTags 组合子从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 是意外缺陷(例如,抛出的异常)。它们应被不同地处理。