已发布 上游基线 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, Exit } 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!"))

Effect.runSyncExit(fail) // => Exit.fail("Oh no!")

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

                ┌─── no error information

Effect<never, never, never>

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

Cause 的变体

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

Empty

Empty cause 表示没有任何错误,由一个空的 reasons 数组表示(Cause.empty)。

Fail

Fail<E> 原因表示由类型为 E 的预期错误导致的失败。只包含这一原因的 Cause<E> 通过 Cause.fail 创建。

Die

Die 原因表示由 defect(即非预期或意料之外的错误)导致的失败。只包含这一原因的 Cause 通过 Cause.die 创建。

Interrupt

Interrupt 原因表示由 Fiber 中断导致的失败,并包含被中断的 Fiber 的数字 id(number | undefined)。只包含这一原因的 Cause 通过 Cause.interrupt 创建。

组合 Cause

Cause<E> 把它的失败原因存储在一个扁平的 reasons 数组中。顺序发生的原因与并发发生的原因使用相同的表示形式。使用 Cause.combine 可以把两个 cause 合并成一个。

示例(把多个失败合并为单个 Cause)

import { Cause } from "effect"

const combined = Cause.combine(Cause.fail("Oh no!"), Cause.die("Boom!"))

combined.reasons.map((reason) => reason._tag) // => ["Fail", "Die"]

获取 Effect 的 Cause

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

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

import { Effect, Exit, Cause } from "effect"

const program = Effect.gen(function* () {
  const exit = yield* Effect.exit(Effect.fail("Oh no!"))
  if (Exit.isFailure(exit)) {
    console.log(exit.cause)
    exit.cause // => Cause.fail("Oh no!")
  }
})

await Effect.runPromise(program)

Guards

要判断 Cause 内部发生了什么,Cause 模块提供了两类 guard:cause 级谓词用于检查一个 Cause 是否包含某种原因,原因级 guard 用于收窄 cause.reasons 中的单个条目。

  • Cause.hasFails:检查 cause 是否至少包含一个预期失败。
  • Cause.hasDies:检查 cause 是否至少包含一个非预期 defect。
  • Cause.hasInterrupts:检查 cause 是否至少包含一次 Fiber 中断。
  • Cause.hasInterruptsOnly:检查 cause 中的每个原因是否都是中断。
  • Cause.isFailReason:把 Reason 收窄为 Fail
  • Cause.isDieReason:把 Reason 收窄为 Die
  • Cause.isInterruptReason:把 Reason 收窄为 Interrupt

空的 cause(没有任何错误)用 cause.reasons.length === 0 检查;并没有专门的 isEmpty 函数。

示例(使用 Guards 识别原因类型)

import { Cause } from "effect"

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

for (const reason of cause.reasons) {
  if (Cause.isFailReason(reason)) {
    console.log(reason.error.message)
    reason.error.message // => "my message"
  }
}

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

格式化原因

若要根据具体的错误场景做出自定义响应,可以遍历 cause.reasons 并对每个原因的 _tag 做 switch。

示例(格式化 Cause 中的每个原因)

import { Cause } from "effect"

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

const formatted = cause.reasons
  .map((reason) => {
    switch (reason._tag) {
      case "Fail":
        return `(error: ${reason.error.message})`
      case "Die":
        return `(defect: ${reason.defect})`
      case "Interrupt":
        return `(fiberId: ${reason.fiberId})`
    }
  })
  .join(", ")

formatted // => "(error: my fail message), (defect: my die message)"

美化输出

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

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

import { Cause } from "effect"

console.log(Cause.pretty(Cause.empty))
/*
Output:
(empty string)
*/
Cause.pretty(Cause.empty) // => ""

console.log(Cause.pretty(Cause.fail(new Error("my fail message"))))
/*
Output:
Error: my fail message
    ...stack trace...
*/
Cause.pretty(Cause.fail(new Error("my fail message"))).split("\n")[0] // => "Error: my fail message"

console.log(Cause.pretty(Cause.die("my die message")))
/*
Output:
Error: my die message
    ...stack trace...
*/
Cause.pretty(Cause.die("my die message")).split("\n")[0] // => "Error: my die message"

console.log(Cause.pretty(Cause.interrupt(1)))
Cause.pretty(Cause.interrupt(1)) // => "InterruptError: All fibers interrupted without error {\n  [cause]: InterruptCause: The fiber was interrupted by:\n      at fiber (#1)\n}"

console.log(
  Cause.pretty(Cause.combine(Cause.fail("fail1"), Cause.fail("fail2"))),
)
/*
Output:
Error: fail1
    ...stack trace...
Error: fail2
    ...stack trace...
*/
Cause.pretty(Cause.combine(Cause.fail("fail1"), Cause.fail("fail2")))
  .split("\n")
  .filter((line) => line.startsWith("Error:")) // => ["Error: fail1", "Error: fail2"]

提取失败与 Defect

Cause.isFailReasonCause.isDieReason 过滤 cause.reasons,就能只检查发生的预期错误或非预期 defect。

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

import { Effect, Cause, Exit } from "effect"

const program = Effect.gen(function* () {
  const exit = yield* Effect.exit(
    Effect.all([
      Effect.fail("error 1"),
      Effect.die("defect"),
      Effect.fail("error 2"),
    ]),
  )
  if (Exit.isFailure(exit)) {
    console.log(
      exit.cause.reasons
        .filter(Cause.isFailReason)
        .map((reason) => reason.error),
    )
    exit.cause.reasons.filter(Cause.isFailReason).map((reason) => reason.error) // => ["error 1"]
    console.log(
      exit.cause.reasons
        .filter(Cause.isDieReason)
        .map((reason) => reason.defect),
    )
    exit.cause.reasons.filter(Cause.isDieReason).map((reason) => reason.defect) // => []
  }
})

await Effect.runPromise(program)