已发布 上游基线 bf46254 原文 ↗ 在 GitHub 编辑

Cause

使用 Effect 中的 Cause 进行全面的错误分析 —— 精确追踪失败、defect 与中断的细节。

Effect<A, E, R> 类型在错误类型 E 上是多态的,这让处理任意期望的错误类型都很灵活。然而,关于失败往往还有更多信息,单靠错误类型 E 是捕获不到的。

为了解决这个问题,Effect 使用 Cause<E> 数据类型来存储各种细节,例如:

  • 非预期的错误或 defect
  • 堆栈与执行轨迹
  • Fiber 被中断的原因

Effect 严格保留所有与失败相关的信息,在 Cause 类型中存储错误上下文的完整图景。这种全面的做法让失败能够被精确地分析和处理,确保不丢失任何数据。

虽然 Cause 值通常不会被直接操作,但它们是 Effect 工作流中错误的底层表示,既能提供并发的错误细节,也能提供顺序的错误细节。需要时,这让你可以对错误做彻底的分析。

创建 Cause

你可以使用 Effect.failCause 有意创建一个带有特定 cause 的 effect。

示例(定义带有不同 Cause 的 Effect)

import { Effect, Cause } from "effect"

// Define an effect that dies with an unexpected error
//
//      ┌─── Effect<never, never, never>
//      ▼
const die = Effect.failCause(Cause.die("Boom!"))

// Define an effect that fails with an expected error
//
//      ┌─── Effect<never, string, never>
//      ▼
const fail = Effect.failCause(Cause.fail("Oh no!"))

有些 cause 不会影响 effect 的错误类型,因此错误通道中会是 never

                ┌─── no error information

Effect<never, never, never>

例如,Cause.die 不会为 effect 指定错误类型,而 Cause.fail 会,并据此设置错误通道的类型。

Cause 的变体

针对各种错误,存在若干种 cause。本节将逐一介绍这些 cause。

Empty

Empty cause 表示没有任何错误。

Fail

Fail<E> cause 表示由类型为 E 的预期错误导致的失败。

Die

Die cause 表示由 defect(即非预期或意料之外的错误)导致的失败。

Interrupt

Interrupt cause 表示由 Fiber 中断导致的失败,并包含被中断的 FiberFiberId

Sequential

Sequential cause 把先后发生的两个 cause 组合在一起。

例如,在 Effect.ensuring 操作(类似于 try-finally)中,如果 tryfinally 两段都失败,这两个错误会由一个 Sequential cause 按顺序表示出来。

示例(用 Sequential Cause 捕获顺序失败)

import { Effect, Cause } from "effect"

const program = Effect.failCause(Cause.fail("Oh no!")).pipe(
  Effect.ensuring(Effect.failCause(Cause.die("Boom!"))),
)

Effect.runPromiseExit(program).then(console.log)
/*
Output:
{
  _id: 'Exit',
  _tag: 'Failure',
  cause: {
    _id: 'Cause',
    _tag: 'Sequential',
    left: { _id: 'Cause', _tag: 'Fail', failure: 'Oh no!' },
    right: { _id: 'Cause', _tag: 'Die', defect: 'Boom!' }
  }
}
*/

Parallel

Parallel cause 把并发发生的两个 cause 组合在一起。

在 Effect 程序中,两个操作可能并行运行,从而可能导致多个失败。当两个计算同时失败时,Parallel cause 就表示 effect 工作流中并发发生的错误。

示例(用 Parallel Cause 捕获并发失败)

import { Effect, Cause } from "effect"

const program = Effect.all(
  [
    Effect.failCause(Cause.fail("Oh no!")),
    Effect.failCause(Cause.die("Boom!")),
  ],
  { concurrency: 2 },
)

Effect.runPromiseExit(program).then(console.log)
/*
Output:
{
  _id: 'Exit',
  _tag: 'Failure',
  cause: {
    _id: 'Cause',
    _tag: 'Parallel',
    left: { _id: 'Cause', _tag: 'Fail', failure: 'Oh no!' },
    right: { _id: 'Cause', _tag: 'Die', defect: 'Boom!' }
  }
}
*/

获取 Effect 的 Cause

要获取一个失败 effect 的 cause,请使用 Effect.cause。这让你可以检查或处理失败背后的确切原因。

示例(获取并检查失败的 Cause)

import { Effect } from "effect"

const program = Effect.gen(function* () {
  const cause = yield* Effect.cause(Effect.fail("Oh no!"))
  console.log(cause)
})

Effect.runPromise(program)
/*
Output:
{ _id: 'Cause', _tag: 'Fail', failure: 'Oh no!' }
*/

类型守卫

要判断一个 Cause 的具体类型,可以使用 Cause 模块提供的 guard:

  • Cause.isEmpty:检查 cause 是否为空,即不存在任何错误。
  • Cause.isFailType:识别表示预期失败的 cause。
  • Cause.isDie:识别表示非预期 defect 的 cause。
  • Cause.isInterruptType:识别与 Fiber 中断相关的 cause。
  • Cause.isSequentialType:检查 cause 是否由顺序发生的错误组成。
  • Cause.isParallelType:检查 cause 是否包含并发发生的错误。

示例(使用 Guard 识别 Cause 的类型)

import { Cause } from "effect"

const cause = Cause.fail(new Error("my message"))

if (Cause.isFailType(cause)) {
  console.log(cause.error.message) // Output: my message
}

这些 guard 让你能准确识别 Cause 的类型,从而更容易在代码中处理各种错误情况。无论是应对预期失败、非预期 defect、中断还是复合错误,这些 guard 都提供了一种清晰的方法来评估和管理错误场景。

模式匹配

Cause.match 函数提供了一种直接的方式来处理 Cause 的每一种情况。通过为每种可能的 cause 类型定义回调,你可以针对具体的错误场景做出自定义的响应。

示例(对不同 Cause 进行模式匹配)

import { Cause } from "effect"

const cause = Cause.parallel(
  Cause.fail(new Error("my fail message")),
  Cause.die("my die message"),
)

console.log(
  Cause.match(cause, {
    onEmpty: "(empty)",
    onFail: (error) => `(error: ${error.message})`,
    onDie: (defect) => `(defect: ${defect})`,
    onInterrupt: (fiberId) => `(fiberId: ${fiberId})`,
    onSequential: (left, right) =>
      `(onSequential (left: ${left}) (right: ${right}))`,
    onParallel: (left, right) =>
      `(onParallel (left: ${left}) (right: ${right})`,
  }),
)
/*
Output:
(onParallel (left: (error: my fail message)) (right: (defect: my die message))
*/

美化输出

清晰易读的错误信息是高效调试的关键。Cause.pretty 函数以结构化的方式格式化错误信息,让你更容易理解失败的细节。

示例(使用 Cause.pretty 获得易读的错误信息)

import { Cause, FiberId } from "effect"

console.log(Cause.pretty(Cause.empty))
/*
Output:
All fibers interrupted without errors.
*/

console.log(Cause.pretty(Cause.fail(new Error("my fail message"))))
/*
Output:
Error: my fail message
    ...stack trace...
*/

console.log(Cause.pretty(Cause.die("my die message")))
/*
Output:
Error: my die message
*/

console.log(Cause.pretty(Cause.interrupt(FiberId.make(1, 0))))
/*
Output:
All fibers interrupted without errors.
*/

console.log(
  Cause.pretty(Cause.sequential(Cause.fail("fail1"), Cause.fail("fail2"))),
)
/*
Output:
Error: fail1
Error: fail2
*/

提取失败与 Defect

要从 Cause 中专门收集失败或 defect,可以使用 Cause.failuresCause.defects。这些函数让你只检查发生的预期错误或非预期 defect。

示例(从 Cause 中提取失败与 Defect)

import { Effect, Cause } from "effect"

const program = Effect.gen(function* () {
  const cause = yield* Effect.cause(
    Effect.all([
      Effect.fail("error 1"),
      Effect.die("defect"),
      Effect.fail("error 2"),
    ]),
  )
  console.log(Cause.failures(cause))
  console.log(Cause.defects(cause))
})

Effect.runPromise(program)
/*
Output:
{ _id: 'Chunk', values: [ 'error 1' ] }
{ _id: 'Chunk', values: [] }
*/