继续上一篇,翻译一下错误处理
两种类型的错误
与其他程序一样,Effect程序可能因预期或意外原因而失败。非Effect程序与Effect程序的区别在于程序失败时提供的详细信息。Effect会尽可能保留导致程序失败的详细信息,生成详细、全面且人类可读的故障消息。
在Effect程序中,程序失败有两种可能方式:
- 预期错误:开发者预料到并作为正常程序执行一部分预期的错误。
- 意外错误:意外发生且不属于预期程序流程的错误。
预期错误
这些错误(也称为_故障_、类型化错误_或_可恢复错误)是开发者作为正常程序执行一部分预期的错误。它们的作用类似于受检异常,并在定义程序领域和控制流程中发挥作用。
预期错误通过Effect数据类型的"Error"通道在类型级别被追踪:
const program: Effect<string, HttpError, never>
从类型中明显看出,该程序可能因HttpError类型的错误而失败。
意外错误
意外错误(也称为_缺陷_、非类型化错误_或_不可恢复错误)是开发者未预料到会在正常程序执行期间发生的错误。与作为程序领域和控制流程一部分的预期错误不同,意外错误类似于未受检异常,超出了程序的预期行为。
由于这些错误未被预期,Effect不会在类型级别追踪它们。但Effect运行时会追踪这些错误,并提供多种方法来帮助从意外错误中恢复。
预期的错误
预期的错误在类型级别通过Effect数据类型在"错误通道"中进行跟踪:
┌─── 表示成功类型
│ ┌─── 表示错误类型
│ │ ┌─── 表示所需依赖项
▼ ▼ ▼
Effect<成功类型, 错误类型, 依赖项>
这意味着Effect类型不仅捕获程序成功时返回的内容,还捕获它可能产生的错误类型。
示例(创建一个可能失败的Effect)
在这个例子中,我们定义了一个可能随机失败并返回HttpError的程序。
import { Effect, Random } from "effect"
class HttpError {
readonly _tag = "HttpError"
}
// ┌─── Effect<string, HttpError, never>
// ▼
const program = Effect.gen(function* () {
// 生成一个0到1之间的随机数
const n = yield* Random.next
// 模拟HTTP错误
if (n < 0.5) {
yield* Effect.fail(new HttpError())
}
return "some result"
})
program的类型告诉我们它要么返回一个string,要么失败并返回HttpError:
const program: Effect<string, HttpError, never>
在这个例子中,我们使用一个类来表示HttpError类型,这允许我们定义错误类型和构造函数。不过,你可以使用任何你喜欢的方式来建模你的错误类型。
值得注意的是,我们给类添加了一个只读的_tag字段:
class HttpError {
// 这个字段作为错误的判别式
readonly _tag = "HttpError"
}
这个判别式字段在我们讨论像Effect.catchTag这样的API时会很有用,这些API有助于处理特定的错误类型。
添加一个判别式字段,比如_tag,有助于在错误处理过程中区分不同类型的错误。 它还可以防止TypeScript合并类型,确保每个错误根据其判别值被唯一对待。
关于如何构造带标签的错误,更多信息请参见Data.TaggedError。
错误跟踪
在Effect中,如果一个程序可能因多种类型的错误而失败,它们会自动被跟踪为这些错误类型的联合。
这让你能够确切知道执行过程中可能发生的错误,使错误处理更加精确和可预测。
下面的例子展示了错误是如何被自动跟踪的。
示例(自动跟踪错误)
import { Effect, Random } from "effect"
class HttpError {
readonly _tag = "HttpError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
// ┌─── Effect<string, HttpError | ValidationError, never>
// ▼
const program = Effect.gen(function* () {
// 生成两个0到1之间的随机数
const n1 = yield* Random.next
const n2 = yield* Random.next
// 模拟HTTP错误
if (n1 < 0.5) {
yield* Effect.fail(new HttpError())
}
// 模拟验证错误
if (n2 < 0.5) {
yield* Effect.fail(new ValidationError())
}
return "some result"
})
Effect自动跟踪程序执行过程中可能发生的错误作为联合类型:
const program: Effect<string, HttpError | ValidationError, never>
表明它可能因HttpError或ValidationError而失败。
短路机制
当使用像Effect.gen、Effect.map、Effect.flatMap和Effect.andThen这样的API时,理解它们如何处理错误非常重要。
这些API设计为在遇到第一个错误时短路执行。
这对开发者意味着什么?假设你有一系列操作或一组需要按顺序执行的effect。如果在执行其中一个effect时发生错误,剩余的计算将被跳过,错误将被传播到最终结果。
简单来说,短路行为确保如果你的程序在任何步骤出现问题,它不会浪费时间执行不必要的计算。相反,它会立即停止并返回错误,让你知道出了问题。
示例(短路行为)
import { Effect, Console } from "effect"
// 定义三个代表不同任务的effect。
const task1 = Console.log("执行task1...")
const task2 = Effect.fail("出错了!")
const task3 = Console.log("执行task3...")
// 组合这三个任务按顺序运行。
// 如果其中一个任务失败,后续任务将不会执行。
const program = Effect.gen(function* () {
yield* task1
// task1之后执行task2,但它因错误而失败
yield* task2
// 这个计算不会执行,因为前一个失败了
yield* task3
})
Effect.runPromiseExit(program).then(console.log)
/*
输出:
执行task1...
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Fail', failure: '出错了!' }
}
*/
这段代码展示了发生错误时的短路行为。
每个操作都依赖于前一个操作的成功执行。
如果发生任何错误,执行将短路,错误会被传播。
在这个具体的例子中,task3永远不会执行,因为task2中发生了错误。
捕获所有错误
either
Effect.either 函数将 Effect<A, E, R> 转换为一个效果,该效果将潜在的失败和成功都封装在 Either 数据类型中:
Effect<A, E, R> -> Effect<Either<A, E>, never, R>
这意味着如果你有一个以下类型的效果:
Effect<string, HttpError, never>
并且你对其调用 Effect.either,类型将变为:
Effect<Either<string, HttpError>, never, never>
生成的效果不会失败,因为潜在的失败现在由 Either 的 Left 类型表示。
返回的 Effect 的错误类型被指定为 never,确认该效果的结构不会失败。
通过生成一个 Either,我们能够对这种类型进行“模式匹配”,以在生成器函数中处理失败和成功的情况。
示例(使用 Effect.either 处理错误)
import { Effect, Either, Random } from "effect"
class HttpError {
readonly _tag = "HttpError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
// ┌─── Effect<string, HttpError | ValidationError, never>
// ▼
const program = Effect.gen(function* () {
const n1 = yield* Random.next
const n2 = yield* Random.next
if (n1 < 0.5) {
yield* Effect.fail(new HttpError())
}
if (n2 < 0.5) {
yield* Effect.fail(new ValidationError())
}
return "some result"
})
// ┌─── Effect<string, never, never>
// ▼
const recovered = Effect.gen(function* () {
// ┌─── Either<string, HttpError | ValidationError>
// ▼
const failureOrSuccess = yield* Effect.either(program)
if (Either.isLeft(failureOrSuccess)) {
// 失败情况:可以从 `left` 属性中提取错误
const error = failureOrSuccess.left
return `Recovering from ${error._tag}`
} else {
// 成功情况:可以从 `right` 属性中提取值
return failureOrSuccess.right
}
})
如你所见,由于所有错误都被处理了,结果效果 recovered 的错误类型为 never:
const recovered: Effect<string, never, never>
我们可以通过使用 Either.match 函数来简化代码,该函数直接接受两个回调函数来处理错误和成功的值:
示例(使用 Either.match 简化)
import { Effect, Either, Random } from "effect"
class HttpError {
readonly _tag = "HttpError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
// ┌─── Effect<string, HttpError | ValidationError, never>
// ▼
const program = Effect.gen(function* () {
const n1 = yield* Random.next
const n2 = yield* Random.next
if (n1 < 0.5) {
yield* Effect.fail(new HttpError())
}
if (n2 < 0.5) {
yield* Effect.fail(new ValidationError())
}
return "some result"
})
// ┌─── Effect<string, never, never>
// ▼
const recovered = Effect.gen(function* () {
// ┌─── Either<string, HttpError | ValidationError>
// ▼
const failureOrSuccess = yield* Effect.either(program)
return Either.match(failureOrSuccess, {
onLeft: (error) => `Recovering from ${error._tag}`,
onRight: (value) => value // 成功情况下不做任何操作
})
})
option
使用 Option 数据类型将效果的成功和失败封装起来。
Effect.option 函数将效果的成功或失败包装在 Option 类型中,使两种情况都显式化。如果原始效果成功,其值会被包装在 Option.some 中。如果失败,错误会被映射为 Option.none。
由于错误类型被设置为 never,最终的效果不会直接失败。然而,致命错误(如缺陷)不会被封装。
示例(使用 Effect.option 处理错误)
import { Effect } from "effect"
const maybe1 = Effect.option(Effect.succeed(1))
Effect.runPromiseExit(maybe1).then(console.log)
/*
输出:
{
_id: 'Exit',
_tag: 'Success',
value: { _id: 'Option', _tag: 'Some', value: 1 }
}
*/
const maybe2 = Effect.option(Effect.fail("Uh oh!"))
Effect.runPromiseExit(maybe2).then(console.log)
/*
输出:
{
_id: 'Exit',
_tag: 'Success',
value: { _id: 'Option', _tag: 'None' }
}
*/
const maybe3 = Effect.option(Effect.die("Boom!"))
Effect.runPromiseExit(maybe3).then(console.log)
/*
输出:
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Die', defect: 'Boom!' }
}
*/
catchAll
通过提供备用效果来处理效果中的所有错误。
Effect.catchAll 函数捕获效果执行过程中可能发生的任何错误,并允许你通过指定备用效果来处理它们。这确保了程序在遇到错误时不会失败,而是通过提供的备用逻辑从错误中恢复。
仅可恢复错误
Effect.catchAll仅处理可恢复错误。它不会从不可恢复的缺陷中恢复。如需处理所有类型的失败,请参阅 Effect.catchAllCause。
示例(为可恢复错误提供恢复逻辑)
import { Effect, Random } from "effect"
class HttpError {
readonly _tag = "HttpError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
// ┌─── Effect<string, HttpError | ValidationError, never>
// ▼
const program = Effect.gen(function* () {
const n1 = yield* Random.next
const n2 = yield* Random.next
if (n1 < 0.5) {
yield* Effect.fail(new HttpError())
}
if (n2 < 0.5) {
yield* Effect.fail(new ValidationError())
}
return "some result"
})
// ┌─── Effect<string, never, never>
// ▼
const recovered = program.pipe(
Effect.catchAll((error) =>
Effect.succeed(`从 ${error._tag} 中恢复`)
)
)
可以看到程序中错误通道的类型已变为 never:
const recovered: Effect<string, never, never>
表示所有错误都已被处理。
catchAllCause
通过提供恢复效果来处理可恢复和不可恢复的错误。
Effect.catchAllCause 函数允许你处理所有错误,包括不可恢复的缺陷,通过提供恢复效果。恢复逻辑基于错误的 Cause,它提供了关于失败的详细信息。
示例(从所有错误中恢复)
import { Cause, Effect } from "effect"
// 定义一个可能因可恢复或不可恢复错误而失败的效果
const program = Effect.fail("出错了!")
// 通过检查原因从所有错误中恢复
const recovered = program.pipe(
Effect.catchAllCause((cause) =>
Cause.isFailType(cause)
? Effect.succeed("从常规错误中恢复")
: Effect.succeed("从缺陷中恢复")
)
)
Effect.runPromise(recovered).then(console.log)
// 输出: "从常规错误中恢复"
何时从缺陷中恢复
缺陷是通常不应恢复的意外错误,因为它们通常表示严重问题。但在某些情况下,如动态加载插件时,可能需要受控恢复。
捕获特定错误
either
之前展示的Effect.either函数不仅可以捕获所有错误,还可以用于捕获特定错误。
通过生成一个Either类型,我们能够在生成器函数中对这种类型进行"模式匹配",从而处理失败和成功两种情况。
示例(使用Effect.either处理特定错误)
import { Effect, Random, Either } from "effect"
class HttpError {
readonly _tag = "HttpError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
// ┌─── Effect<string, HttpError | ValidationError, never>
// ▼
const program = Effect.gen(function* () {
const n1 = yield* Random.next
const n2 = yield* Random.next
if (n1 < 0.5) {
yield* Effect.fail(new HttpError())
}
if (n2 < 0.5) {
yield* Effect.fail(new ValidationError())
}
return "some result"
})
// ┌─── Effect<string, ValidationError, never>
// ▼
const recovered = Effect.gen(function* () {
const failureOrSuccess = yield* Effect.either(program)
if (Either.isLeft(failureOrSuccess)) {
const error = failureOrSuccess.left
// 仅处理HttpError错误
if (error._tag === "HttpError") {
return "从HttpError恢复"
} else {
// 重新抛出ValidationError
return yield* Effect.fail(error)
}
} else {
return failureOrSuccess.right
}
})
我们可以观察到程序错误通道中的类型已变为仅显示ValidationError:
const recovered: Effect<string, ValidationError, never>
这表明HttpError已被处理。
如果我们还想处理ValidationError,可以轻松地在代码中添加另一个情况:
import { Effect, Random, Either } from "effect"
class HttpError {
readonly _tag = "HttpError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
const program = Effect.gen(function* () {
const n1 = yield* Random.next
const n2 = yield* Random.next
if (n1 < 0.5) {
yield* Effect.fail(new HttpError())
}
if (n2 < 0.5) {
yield* Effect.fail(new ValidationError())
}
return "some result"
})
// ┌─── Effect<string, never, never>
// ▼
const recovered = Effect.gen(function* () {
const failureOrSuccess = yield* Effect.either(program)
if (Either.isLeft(failureOrSuccess)) {
const error = failureOrSuccess.left
// 同时处理HttpError和ValidationError
if (error._tag === "HttpError") {
return "从HttpError恢复"
} else {
return "从ValidationError恢复"
}
} else {
return failureOrSuccess.right
}
})
我们可以观察到错误通道中的类型已变为never:
const recovered: Effect<string, never, never>
这表明所有错误都已被处理。
catchSome
捕获并恢复特定类型的错误,允许您仅针对某些错误尝试恢复。
Effect.catchSome 允许您通过为特定错误提供恢复效果,有选择地捕获和处理某些类型的错误。如果错误匹配条件,则尝试恢复;如果不匹配,则不影响程序。此函数不会改变错误类型,意味着错误类型与原始效果保持一致。
示例(使用 Effect.catchSome 处理特定错误)
import { Effect, Random, Option } from "effect"
class HttpError {
readonly _tag = "HttpError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
// ┌─── Effect<string, HttpError | ValidationError, never>
// ▼
const program = Effect.gen(function* () {
const n1 = yield* Random.next
const n2 = yield* Random.next
if (n1 < 0.5) {
yield* Effect.fail(new HttpError())
}
if (n2 < 0.5) {
yield* Effect.fail(new ValidationError())
}
return "some result"
})
// ┌─── Effect<string, HttpError | ValidationError, never>
// ▼
const recovered = program.pipe(
Effect.catchSome((error) => {
// 仅处理 HttpError 错误
if (error._tag === "HttpError") {
return Option.some(Effect.succeed("从 HttpError 恢复"))
} else {
return Option.none()
}
})
)
在上面的代码中,Effect.catchSome 接受一个函数,该函数检查错误并决定是否尝试恢复。如果错误匹配特定条件,可以通过返回 Option.some(effect) 尝试恢复。如果无法恢复,只需返回 Option.none()。
需要注意的是,虽然 Effect.catchSome 允许您捕获特定错误,但它不会改变错误类型本身。因此,最终的效果仍将与原始效果具有相同的错误类型:
const recovered: Effect<string, HttpError | ValidationError, never>
catchIf
基于谓词从特定错误中恢复。
Effect.catchIf 的工作方式类似于 Effect.catchSome,但它允许您通过提供谓词函数从错误中恢复。如果谓词匹配错误,则应用恢复效果。此函数不会改变错误类型,因此最终的效果仍将携带原始错误类型,除非使用用户定义的类型守卫来缩小类型。
示例(使用谓词捕获特定错误)
import { Effect, Random } from "effect"
class HttpError {
readonly _tag = "HttpError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
// ┌─── Effect<string, HttpError | ValidationError, never>
// ▼
const program = Effect.gen(function* () {
const n1 = yield* Random.next
const n2 = yield* Random.next
if (n1 < 0.5) {
yield* Effect.fail(new HttpError())
}
if (n2 < 0.5) {
yield* Effect.fail(new ValidationError())
}
return "some result"
})
// ┌─── Effect<string, ValidationError, never>
// ▼
const recovered = program.pipe(
Effect.catchIf(
// 仅处理 HttpError 错误
(error) => error._tag === "HttpError",
() => Effect.succeed("从 HttpError 恢复")
)
)
需要注意的是,对于 TypeScript 版本 < 5.5,虽然 Effect.catchIf 允许您捕获特定错误,但它不会改变错误类型本身。因此,最终的效果仍将与原始效果具有相同的错误类型:
const recovered: Effect<string, HttpError | ValidationError, never>
在 TypeScript 版本 >= 5.5 中,改进的类型缩小会导致推断出的错误类型为 ValidationError。
TypeScript 版本 < 5.5 的解决方案
如果您提供用户定义的类型守卫而不是谓词,则最终的错误类型将被修剪,返回 Effect<string, ValidationError, never>:
import { Effect, Random } from "effect"
class HttpError {
readonly _tag = "HttpError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
// ┌─── Effect<string, HttpError | ValidationError, never>
// ▼
const program = Effect.gen(function* () {
const n1 = yield* Random.next
const n2 = yield* Random.next
if (n1 < 0.5) {
yield* Effect.fail(new HttpError())
}
if (n2 < 0.5) {
yield* Effect.fail(new ValidationError())
}
return "some result"
})
// ┌─── Effect<string, ValidationError, never>
// ▼
const recovered = program.pipe(
Effect.catchIf(
// 用户定义的类型守卫
(error): error is HttpError => error._tag === "HttpError",
() => Effect.succeed("从 HttpError 恢复")
)
)
catchTag
通过错误的 _tag 字段捕获并处理特定错误,该字段用作区分标识。
当你的错误带有标识错误类型的 _tag 字段时,Effect.catchTag 非常有用。你可以通过匹配 _tag 值来使用此函数处理特定的错误类型。这允许精确的错误处理,确保仅捕获和处理特定的错误。
错误类型必须具有 _tag 字段才能使用 Effect.catchTag。此字段用于识别和匹配错误。
示例(按标签处理错误)
import { Effect, Random } from "effect"
class HttpError {
readonly _tag = "HttpError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
// ┌─── Effect<string, HttpError | ValidationError, never>
// ▼
const program = Effect.gen(function* () {
const n1 = yield* Random.next
const n2 = yield* Random.next
if (n1 < 0.5) {
yield* Effect.fail(new HttpError())
}
if (n2 < 0.5) {
yield* Effect.fail(new ValidationError())
}
return "some result"
})
// ┌─── Effect<string, ValidationError, never>
// ▼
const recovered = program.pipe(
// 仅处理 HttpError 错误
Effect.catchTag("HttpError", (_HttpError) =>
Effect.succeed("从 HttpError 恢复")
)
)
在上面的示例中,Effect.catchTag 函数允许我们专门处理 HttpError。如果在程序执行过程中发生 HttpError,将调用提供的错误处理函数,程序将继续执行处理程序中指定的恢复逻辑。
我们可以观察到程序错误通道中的类型已更改为仅显示 ValidationError:
const recovered: Effect<string, ValidationError, never>
这表明 HttpError 已被处理。
如果我们还想处理 ValidationError,可以简单地添加另一个 catchTag:
示例(使用 catchTag 处理多种错误类型)
import { Effect, Random } from "effect"
class HttpError {
readonly _tag = "HttpError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
// ┌─── Effect<string, HttpError | ValidationError, never>
// ▼
const program = Effect.gen(function* () {
const n1 = yield* Random.next
const n2 = yield* Random.next
if (n1 < 0.5) {
yield* Effect.fail(new HttpError())
}
if (n2 < 0.5) {
yield* Effect.fail(new ValidationError())
}
return "some result"
})
// ┌─── Effect<string, never, never>
// ▼
const recovered = program.pipe(
// 同时处理 HttpError 和 ValidationError
Effect.catchTag("HttpError", (_HttpError) =>
Effect.succeed("从 HttpError 恢复")
),
Effect.catchTag("ValidationError", (_ValidationError) =>
Effect.succeed("从 ValidationError 恢复")
)
)
我们可以观察到程序错误通道中的类型已更改为 never:
const recovered: Effect<string, never, never>
这表明所有错误都已被处理。
错误类型必须具有只读的 _tag 字段才能使用 catchTag。此字段用于识别和匹配错误。
catchTags
在单个代码块中使用错误的 _tag 字段处理多个错误。
Effect.catchTags 是一种方便的方法,可以一次性处理多种错误类型。无需多次使用 Effect.catchTag,你可以传递一个对象,其中每个键是错误类型的 _tag,值是该特定错误的处理程序。这允许你在单个调用中捕获并恢复多种错误类型。
示例(一次性处理多个带标签的错误类型)
import { Effect, Random } from "effect"
class HttpError {
readonly _tag = "HttpError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
// ┌─── Effect<string, HttpError | ValidationError, never>
// ▼
const program = Effect.gen(function* () {
const n1 = yield* Random.next
const n2 = yield* Random.next
if (n1 < 0.5) {
yield* Effect.fail(new HttpError())
}
if (n2 < 0.5) {
yield* Effect.fail(new ValidationError())
}
return "some result"
})
// ┌─── Effect<string, never, never>
// ▼
const recovered = program.pipe(
Effect.catchTags({
HttpError: (_HttpError) =>
Effect.succeed(`从 HttpError 恢复`),
ValidationError: (_ValidationError) =>
Effect.succeed(`从 ValidationError 恢复`)
})
)
此函数接受一个对象,其中每个属性代表一个特定的错误 _tag(在本例中为 "HttpError" 和 "ValidationError"),对应的值是在发生该特定错误时要执行的错误处理函数。
错误类型必须具有只读的 _tag 字段才能使用 catchTag。此字段用于识别和匹配错误。
Effect.fn
Effect.fn 函数允许您创建返回效果的追踪函数。它提供两个关键特性:
- 带位置详情的调用栈追踪(当错误发生时)
- 自动创建追踪区间(当提供区间名称时,用于追踪)
如果第一个参数传入区间名称,函数执行将以该名称被追踪。
若未提供名称,调用栈追踪仍有效,但不会创建追踪区间。
可通过以下方式定义函数:
- 生成器函数(允许使用
yield*进行效果组合) - 直接返回
Effect的普通函数
示例(创建带追踪区间名的追踪函数)
import { Effect } from "effect"
const myfunc = Effect.fn("myspan")(function* <N extends number>(n: N) {
yield* Effect.annotateCurrentSpan("n", n) // 向区间附加元数据
console.log(`got: ${n}`)
yield* Effect.fail(new Error("Boom!")) // 模拟失败
})
Effect.runFork(myfunc(100).pipe(Effect.catchAllCause(Effect.logError)))
/*
输出:
got: 100
timestamp=... level=ERROR fiber=#0 cause="Error: Boom!
at <anonymous> (/.../index.ts:6:22) <= 错误抛出位置
at myspan (/.../index.ts:3:23) <= 函数定义位置
at myspan (/.../index.ts:9:16)" <= 调用位置
*/
导出追踪区间
Effect.fn 会自动创建追踪区间。这些区间会捕获函数执行的详细信息,包括元数据和错误数据。
示例(将追踪区间导出到控制台)
import { Effect } from "effect"
import { NodeSdk } from "@effect/opentelemetry"
import {
ConsoleSpanExporter,
BatchSpanProcessor
} from "@opentelemetry/sdk-trace-base"
const myfunc = Effect.fn("myspan")(function* <N extends number>(n: N) {
yield* Effect.annotateCurrentSpan("n", n)
console.log(`got: ${n}`)
yield* Effect.fail(new Error("Boom!"))
})
const program = myfunc(100)
const NodeSdkLive = NodeSdk.layer(() => ({
resource: { serviceName: "example" },
// 将区间数据导出到控制台
spanProcessor: new BatchSpanProcessor(new ConsoleSpanExporter())
}))
Effect.runFork(program.pipe(Effect.provide(NodeSdkLive)))
/*
输出:
got: 100
{
resource: {
attributes: {
'service.name': 'example',
'telemetry.sdk.language': 'nodejs',
'telemetry.sdk.name': '@effect/opentelemetry',
'telemetry.sdk.version': '1.30.1'
}
},
instrumentationScope: { name: 'example', version: undefined, schemaUrl: undefined },
traceId: '22801570119e57a6e2aacda3dec9665b',
parentId: undefined,
traceState: undefined,
name: 'myspan',
id: '7af530c1e01bc0cb',
kind: 0,
timestamp: 1741182277518402.2,
duration: 4300.416,
attributes: {
n: 100,
'code.stacktrace': 'at <anonymous> (/.../index.ts:8:23)\n' +
'at <anonymous> (/.../index.ts:14:17)'
},
status: { code: 2, message: 'Boom!' },
events: [
{
name: 'exception',
attributes: {
'exception.type': 'Error',
'exception.message': 'Boom!',
'exception.stacktrace': 'Error: Boom!\n' +
' at <anonymous> (/.../index.ts:11:22)\n' +
' at myspan (/.../index.ts:8:23)\n' +
' at myspan (/.../index.ts:14:17)'
},
time: [ 1741182277, 522702583 ],
droppedAttributesCount: 0
}
],
links: []
}
*/
将 Effect.fn 作为管道函数使用
Effect.fn 也可作为管道函数使用,允许您在函数定义后创建处理管道,以生成器函数返回的效果作为管道的起始值。
示例(创建带延迟的追踪函数)
import { Effect } from "effect"
const myfunc = Effect.fn(
function* (n: number) {
console.log(`got: ${n}`)
yield* Effect.fail(new Error("Boom!"))
},
// 可同时访问创建的效果和原始参数
(effect, n) => Effect.delay(effect, `${n / 100} seconds`)
)
Effect.runFork(myfunc(100).pipe(Effect.catchAllCause(Effect.logError)))
/*
输出:
got: 100
timestamp=... level=ERROR fiber=#0 cause="Error: Boom! (<= 1秒后出现)
*/
意外错误
在某些情况下,你可能会遇到意外的错误,这时需要决定如何处理它们。Effect 提供了一些函数来帮助你应对这些场景,让你能够在执行效果时发生错误时采取适当的措施。
创建不可恢复的错误
与使用诸如 Effect.fail 这样的组合器来创建类型为 Effect<never, E, never> 的值类似,Effect 库也提供了工具来创建缺陷(defects)。
在处理从业务逻辑角度无法恢复的错误时,创建缺陷是一种常见需求。例如,在多次重试后仍无法建立连接的情况下,终止效果的执行并通过标准输出或外部监控服务进行报告可能是最佳解决方案。
以下函数和组合器允许终止效果,通常用于将类型为 Effect<A, E, R> 的值转换为类型为 Effect<A, never, R> 的值,从而为程序员提供一种逃避机制,避免必须处理和恢复那些无法合理恢复的错误。
die
创建一个终止纤程(fiber)并指定错误的效果。
当代码中遇到不应作为常规错误处理而代表不可恢复缺陷的意外条件时,使用 Effect.die。
Effect.die 函数用于表示一个缺陷,即代码中的关键且意外的错误。调用时,它会生成一个不处理错误而是终止纤程的效果。
生成的效果的错误通道类型为 never,表示无法从该失败中恢复。
示例(在除零时终止并指定错误)
import { Effect } from "effect"
const divide = (a: number, b: number) =>
b === 0
? Effect.die(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
// ┌─── Effect<number, never, never>
// ▼
const program = divide(1, 0)
Effect.runPromise(program).catch(console.error)
/*
输出:
(FiberFailure) Error: Cannot divide by zero
...堆栈跟踪...
*/
dieMessage
创建一个终止纤程并附带指定消息的 RuntimeException 的效果。
当你想因不可恢复的缺陷终止纤程并在消息中包含清晰说明时,使用 Effect.dieMessage。
Effect.dieMessage 函数用于表示一个缺陷,即代码中的关键且意外的错误。调用时,它会生成一个终止纤程的效果,并附带一个包含给定消息的 RuntimeException。
生成的效果的错误通道类型为 never,表示它不处理或恢复错误。
示例(在除零时终止并指定消息)
import { Effect } from "effect"
const divide = (a: number, b: number) =>
b === 0
? Effect.dieMessage("Cannot divide by zero")
: Effect.succeed(a / b)
// ┌─── Effect<number, never, never>
// ▼
const program = divide(1, 0)
Effect.runPromise(program).catch(console.error)
/*
输出:
(FiberFailure) RuntimeException: Cannot divide by zero
...堆栈跟踪...
*/
将失败转换为缺陷
orDie
将效果的失败转换为纤程终止,并从效果的类型中移除错误。
当失败应被视为不可恢复的缺陷且无需错误处理时,使用 Effect.orDie。
Effect.orDie 函数用于当你遇到不想处理或恢复的错误时。它会从效果中移除错误类型,并确保任何失败都会终止纤程。这对于将失败作为缺陷传播非常有用,表明不应在效果内处理这些错误。
示例(将错误作为缺陷传播)
import { Effect } from "effect"
const divide = (a: number, b: number) =>
b === 0
? Effect.fail(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
// ┌─── Effect<number, never, never>
// ▼
const program = Effect.orDie(divide(1, 0))
Effect.runPromise(program).catch(console.error)
/*
输出:
(FiberFailure) Error: Cannot divide by zero
...堆栈跟踪...
*/
orDieWith
将效果的失败转换为纤程终止,并附带自定义错误。
当失败应作为缺陷终止纤程,并且你想为清晰或调试目的自定义错误时,使用 Effect.orDieWith。
Effect.orDieWith 函数的行为类似于 Effect.orDie,但它允许你提供一个映射函数,在终止纤程之前转换错误。这对于希望在将失败作为缺陷传播时包含更详细或用户友好错误的情况非常有用。
示例(自定义缺陷)
import { Effect } from "effect"
const divide = (a: number, b: number) =>
b === 0
? Effect.fail(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
// ┌─── Effect<number, never, never>
// ▼
const program = Effect.orDieWith(
divide(1, 0),
(error) => new Error(`defect: ${error.message}`)
)
Effect.runPromise(program).catch(console.error)
/*
输出:
(FiberFailure) Error: defect: Cannot divide by zero
...堆栈跟踪...
*/
捕获所有缺陷
从缺陷中恢复并没有合理的方式。我们即将讨论的这些函数应该仅用于 Effect 与外部系统之间的边界,用于传输缺陷信息以进行诊断或解释目的。
exit
Effect.exit 函数将一个 Effect<A, E, R> 转换为一个将潜在失败和成功都封装在 Exit 数据类型中的效果:
Effect<A, E, R> -> Effect<Exit<A, E>, never, R>
这意味着如果你有一个以下类型的 effect:
Effect<string, HttpError, never>
然后你对其调用 Effect.exit,类型将变为:
Effect<Exit<string, HttpError>, never, never>
生成的效果不会失败,因为潜在的失败现在由 Exit 的 Failure 类型表示。返回效果的 error 类型被指定为 never,确认该效果的结构不会失败。
通过生成一个 Exit,我们获得了对这种类型进行“模式匹配”的能力,以在生成器函数中处理失败和成功的情况。
示例(使用 Effect.exit 捕获缺陷)
import { Effect, Cause, Console, Exit } from "effect"
// 模拟运行时错误
const task = Effect.dieMessage("Boom!")
const program = Effect.gen(function* () {
const exit = yield* Effect.exit(task)
if (Exit.isFailure(exit)) {
const cause = exit.cause
if (
Cause.isDieType(cause) &&
Cause.isRuntimeException(cause.defect)
) {
yield* Console.log(
`捕获到 RuntimeException 缺陷: ${cause.defect.message}`
)
} else {
yield* Console.log("捕获到未知失败。")
}
}
})
// 因为我们捕获了所有失败,所以得到一个 Exit.Success
Effect.runPromiseExit(program).then(console.log)
/*
输出:
捕获到 RuntimeException 缺陷: Boom!
{
_id: "Exit",
_tag: "Success",
value: undefined
}
*/
catchAllDefect
使用提供的恢复函数从所有缺陷中恢复。
Effect.catchAllDefect 允许你处理缺陷,这些缺陷是通常导致程序终止的意外错误。该函数通过提供一个处理错误的函数,让你可以从这些缺陷中恢复。
然而,它不处理预期错误(如来自 Effect.fail 的错误)或执行中断(如来自 Effect.interrupt 的中断)。
示例(处理所有缺陷)
import { Effect, Cause, Console } from "effect"
// 模拟运行时错误
const task = Effect.dieMessage("Boom!")
const program = Effect.catchAllDefect(task, (defect) => {
if (Cause.isRuntimeException(defect)) {
return Console.log(
`捕获到 RuntimeException 缺陷: ${defect.message}`
)
}
return Console.log("捕获到未知缺陷。")
})
// 因为我们捕获了所有缺陷,所以得到一个 Exit.Success
Effect.runPromiseExit(program).then(console.log)
/*
输出:
捕获到 RuntimeException 缺陷: Boom!
{
_id: "Exit",
_tag: "Success",
value: undefined
}
*/
缺陷是通常不应从中恢复的意外错误,因为它们通常表示严重问题。然而,在某些情况下,例如动态加载插件时,可能需要受控的恢复。
捕获部分缺陷
catchSomeDefect
使用提供的部分函数从特定缺陷中恢复。
Effect.catchSomeDefect 允许你处理特定缺陷,这些缺陷是可能导致程序停止的意外错误。它使用一个部分函数仅捕获某些缺陷而忽略其他缺陷。
然而,它不处理预期错误(如来自 Effect.fail 的错误)或执行中断(如来自 Effect.interrupt 的中断)。
提供给 Effect.catchSomeDefect 的函数充当缺陷的过滤器和处理程序:
- 它接收缺陷作为输入。
- 如果缺陷符合特定条件(例如某种错误类型),函数返回包含恢复逻辑的
Option.some。 - 如果缺陷不符合,函数返回
Option.none,允许缺陷传播。
示例(处理特定缺陷)
import { Effect, Cause, Option, Console } from "effect"
// 模拟运行时错误
const task = Effect.dieMessage("Boom!")
const program = Effect.catchSomeDefect(task, (defect) => {
if (Cause.isIllegalArgumentException(defect)) {
return Option.some(
Console.log(
`捕获到 IllegalArgumentException 缺陷: ${defect.message}`
)
)
}
return Option.none()
})
// 因为我们只捕获 IllegalArgumentException
// 而我们模拟的是运行时错误,所以会得到一个 Exit.Failure。
Effect.runPromiseExit(program).then(console.log)
/*
输出:
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Die',
defect: { _tag: 'RuntimeException' }
}
}
*/
何时从缺陷中恢复
缺陷是通常不应从中恢复的意外错误,因为它们通常表示严重问题。然而,在某些情况下,例如动态加载插件时,可能需要受控的恢复。
回退
本页详细介绍了在Effect库中处理失败和创建回退机制的各种方法。
orElse
Effect.orElse允许您尝试运行一个效果,如果失败,则可以提供一个备用效果来替代执行。
这种方法通过定义在第一个效果遇到错误时执行的替代效果,优雅地处理失败场景。
示例(使用Effect.orElse处理回退)
import { Effect } from "effect"
const success = Effect.succeed("success")
const failure = Effect.fail("failure")
const fallback = Effect.succeed("fallback")
// 首先尝试成功效果,回退效果未被使用
const program1 = Effect.orElse(success, () => fallback)
console.log(Effect.runSync(program1))
// 输出: "success"
// 首先尝试失败效果,回退效果被使用
const program2 = Effect.orElse(failure, () => fallback)
console.log(Effect.runSync(program2))
// 输出: "fallback"
orElseFail
Effect.orElseFail允许您用一个自定义的失败值替换某个效果的失败结果。如果效果失败,您可以提供一个新失败值来替代原始值。
此函数仅适用于失败的效果。如果效果成功,则保持不变。
示例(使用Effect.orElseFail替换失败值)
import { Effect } from "effect"
const validate = (age: number): Effect.Effect<number, string> => {
if (age < 0) {
return Effect.fail("NegativeAgeError")
} else if (age < 18) {
return Effect.fail("IllegalAgeError")
} else {
return Effect.succeed(age)
}
}
const program = Effect.orElseFail(validate(-1), () => "invalid age")
console.log(Effect.runSyncExit(program))
/*
输出:
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Fail', failure: 'invalid age' }
}
*/
orElseSucceed
Effect.orElseSucceed允许您将效果的失败替换为一个成功值。如果效果失败,它将转而成功返回提供的值,确保效果总是成功完成。
这在您希望无论原始效果是否失败都能保证成功结果时非常有用。
该函数确保任何失败都被有效地"吞没"并替换为一个成功值,这有助于在失败时提供默认值。
此函数仅适用于失败的效果。如果效果已经成功,则保持不变。
示例(使用Effect.orElseSucceed将失败替换为成功)
import { Effect } from "effect"
const validate = (age: number): Effect.Effect<number, string> => {
if (age < 0) {
return Effect.fail("NegativeAgeError")
} else if (age < 18) {
return Effect.fail("IllegalAgeError")
} else {
return Effect.succeed(age)
}
}
const program = Effect.orElseSucceed(validate(-1), () => 18)
console.log(Effect.runSyncExit(program))
/*
输出:
{ _id: 'Exit', _tag: 'Success', value: 18 }
*/
firstSuccessOf
Effect.firstSuccessOf允许您按顺序尝试多个效果,一旦其中一个成功,就返回该结果。如果所有效果都失败,则返回列表中最后一个效果的错误。
这在您有多个可能的选择并希望使用第一个有效的方法时非常有用。
该函数是顺序执行的,意味着可迭代对象中的Effect值将按顺序执行,第一个成功的值将决定结果Effect值的输出。
空收集错误
如果提供给
Effect.firstSuccessOf函数的集合为空,它将抛出IllegalArgumentException错误。
示例(查找配置的回退方案)
在此示例中,我们尝试从不同节点检索配置。如果主节点失败,则回退到其他节点,直到找到成功的配置。
import { Effect, Console } from "effect"
interface Config {
host: string
port: number
apiKey: string
}
// 创建带有示例值的配置对象
const makeConfig = (name: string): Config => ({
host: `${name}.example.com`,
port: 8080,
apiKey: "12345-abcde"
})
// 模拟从远程节点检索配置
const remoteConfig = (name: string): Effect.Effect<Config, Error> =>
Effect.gen(function* () {
// 模拟只有node3有可用配置
if (name === "node3") {
yield* Console.log(`Config for ${name} found`)
return makeConfig(name)
} else {
yield* Console.log(`Unavailable config for ${name}`)
return yield* Effect.fail(new Error(`Config not found for ${name}`))
}
})
// 定义主配置和可能的回退节点
const masterConfig = remoteConfig("master")
const nodeConfigs = ["node1", "node2", "node3", "node4"].map(remoteConfig)
// 尝试查找可用的配置,
// 从主节点开始,然后回退到其他节点
const config = Effect.firstSuccessOf([masterConfig, ...nodeConfigs])
// 运行效果以检索配置
const result = Effect.runSync(config)
console.log(result)
/*
输出:
Unavailable config for master
Unavailable config for node1
Unavailable config for node2
Config for node3 found
{ host: 'node3.example.com', port: 8080, apiKey: '12345-abcde' }
*/
匹配
在 Effect 模块中,类似于 Option 和 Exit 等其他模块,我们有一个 Effect.match 函数,可以同时处理不同的情况。
此外,Effect 还提供了多种函数来管理有效程序中的成功和失败场景。
match
Effect.match 允许你为成功和失败场景定义自定义处理程序。你可以提供单独的函数来处理每种情况,从而在效果成功时处理结果,或在效果失败时处理错误。
这对于构建代码结构以不同方式响应成功或失败而不触发副作用非常有用。
示例(处理成功和失败情况)
import { Effect } from "effect"
const success: Effect.Effect<number, Error> = Effect.succeed(42)
const program1 = Effect.match(success, {
onFailure: (error) => `failure: ${error.message}`,
onSuccess: (value) => `success: ${value}`
})
// 运行并记录成功效果的结果
Effect.runPromise(program1).then(console.log)
// 输出: "success: 42"
const failure: Effect.Effect<number, Error> = Effect.fail(
new Error("Uh oh!")
)
const program2 = Effect.match(failure, {
onFailure: (error) => `failure: ${error.message}`,
onSuccess: (value) => `success: ${value}`
})
// 运行并记录失败效果的结果
Effect.runPromise(program2).then(console.log)
// 输出: "failure: Uh oh!"
ignore
Effect.ignore 允许你运行一个效果而不关心其结果,无论成功还是失败。
这在你只关心效果的副作用而不需要处理或处理其结果时非常有用。
示例(使用 Effect.ignore 丢弃值)
import { Effect } from "effect"
// ┌─── Effect<number, string, never>
// ▼
const task = Effect.fail("Uh oh!").pipe(Effect.as(5))
// ┌─── Effect<void, never, never>
// ▼
const program = Effect.ignore(task)
matchEffect
Effect.matchEffect 函数类似于 Effect.match,但它允许你在成功和失败的处理程序中执行副作用。
这在你需要根据效果的成功或失败执行额外操作(如记录日志或通知用户)时非常有用。
示例(处理带有副作用的成功和失败)
import { Effect } from "effect"
const success: Effect.Effect<number, Error> = Effect.succeed(42)
const failure: Effect.Effect<number, Error> = Effect.fail(
new Error("Uh oh!")
)
const program1 = Effect.matchEffect(success, {
onFailure: (error) =>
Effect.succeed(`failure: ${error.message}`).pipe(
Effect.tap(Effect.log)
),
onSuccess: (value) =>
Effect.succeed(`success: ${value}`).pipe(Effect.tap(Effect.log))
})
console.log(Effect.runSync(program1))
/*
输出:
timestamp=... level=INFO fiber=#0 message="success: 42"
success: 42
*/
const program2 = Effect.matchEffect(failure, {
onFailure: (error) =>
Effect.succeed(`failure: ${error.message}`).pipe(
Effect.tap(Effect.log)
),
onSuccess: (value) =>
Effect.succeed(`success: ${value}`).pipe(Effect.tap(Effect.log))
})
console.log(Effect.runSync(program2))
/*
输出:
timestamp=... level=INFO fiber=#1 message="failure: Uh oh!"
failure: Uh oh!
*/
matchCause
Effect.matchCause 函数允许你在处理失败时访问 fiber 内的完整 cause。
这对于区分不同类型的错误(如常规失败、缺陷或中断)非常有用。你可以根据原因提供特定的处理逻辑。
示例(处理不同的失败原因)
import { Effect } from "effect"
const task: Effect.Effect<number, Error> = Effect.die("Uh oh!")
const program = Effect.matchCause(task, {
onFailure: (cause) => {
switch (cause._tag) {
case "Fail":
// 处理标准失败
return `Fail: ${cause.error.message}`
case "Die":
// 处理缺陷(意外错误)
return `Die: ${cause.defect}`
case "Interrupt":
// 处理中断
return `${cause.fiberId} interrupted!`
}
// 其他原因的备用处理
return "failed due to other causes"
},
onSuccess: (value) =>
// 任务成功完成
`succeeded with ${value} value`
})
Effect.runPromise(program).then(console.log)
// 输出: "Die: Uh oh!"
matchCauseEffect
Effect.matchCauseEffect 函数的工作方式类似于 Effect.matchCause,但它还允许你根据失败原因执行额外的副作用。
该函数提供了对失败的完整 cause 的访问,使得可以区分各种失败类型,并允许你在执行副作用(如记录日志或其他操作)时做出相应的响应。
示例(处理带有副作用的不同失败原因)
import { Effect, Console } from "effect"
const task: Effect.Effect<number, Error> = Effect.die("Uh oh!")
const program = Effect.matchCauseEffect(task, {
onFailure: (cause) => {
switch (cause._tag) {
case "Fail":
// 处理标准失败并记录日志消息
return Console.log(`Fail: ${cause.error.message}`)
case "Die":
// 处理缺陷(意外错误)并记录缺陷
return Console.log(`Die: ${cause.defect}`)
case "Interrupt":
// 处理中断并记录被中断的 fiberId
return Console.log(`${cause.fiberId} interrupted!`)
}
// 其他原因的备用处理
return Console.log("failed due to other causes")
},
onSuccess: (value) =>
// 如果任务成功完成,记录成功日志
Console.log(`succeeded with ${value} value`)
})
Effect.runPromise(program)
// 输出: "Die: Uh oh!"
重试
在软件开发中,经常会遇到由于网络问题、资源不可用或外部依赖等因素导致操作暂时失败的情况。此时,通常需要自动重试该操作,使其最终能够成功执行。
重试机制是处理瞬时故障并确保关键操作成功执行的强大工具。在 Effect 中,通过内置函数和调度策略,重试操作变得简单而灵活。
本指南将探讨 Effect 中的重试概念,学习如何使用 retry 和 retryOrElse 函数处理失败场景。我们将了解如何通过调度策略定义重试策略,决定何时以及重试多少次操作。
无论您在处理网络请求、数据库交互还是其他容易出错的操作,掌握 Effect 的重试功能都能显著提升应用程序的健壮性和可靠性。
retry
Effect.retry 函数接受一个 effect 和一个调度策略,会根据策略规则在 effect 失败时自动重试。
若 effect 最终成功,则返回结果。
若达到最大重试次数后 effect 仍失败,则传播该失败。
这在处理间歇性故障(如网络问题或临时资源不可用)时非常有用。通过定义重试策略,您可以控制重试次数、重试间隔以及何时停止重试。
示例(固定延迟重试)
import { Effect, Schedule } from "effect"
let count = 0
// 模拟可能失败的 effect
const task = Effect.async<string, Error>((resume) => {
if (count <= 2) {
count++
console.log("failure")
resume(Effect.fail(new Error()))
} else {
console.log("success")
resume(Effect.succeed("yay!"))
}
})
// 使用固定延迟定义重试策略
const policy = Schedule.fixed("100 millis")
const repeated = Effect.retry(task, policy)
Effect.runPromise(repeated).then(console.log)
/*
输出:
failure
failure
failure
success
yay!
*/
立即重试 n 次
也可以通过更简单的策略立即重试失败 effect 指定次数:
示例(最多重试任务 5 次)
import { Effect } from "effect"
let count = 0
// 模拟可能失败的 effect
const task = Effect.async<string, Error>((resume) => {
if (count <= 2) {
count++
console.log("failure")
resume(Effect.fail(new Error()))
} else {
console.log("success")
resume(Effect.succeed("yay!"))
}
})
// 最多重试任务 5 次
Effect.runPromise(Effect.retry(task, { times: 5 }))
/*
输出:
failure
failure
failure
success
*/
基于条件重试
可以通过指定条件自定义重试管理方式。使用 until 或 while 选项控制何时停止重试。
示例(重试直到满足特定条件)
import { Effect } from "effect"
let count = 0
// 定义一个每次调用模拟不同错误的 effect
const action = Effect.failSync(() => {
console.log(`Action called ${++count} time(s)`)
return `Error ${count}`
})
// 重试 action 直到满足特定条件
const program = Effect.retry(action, {
until: (err) => err === "Error 3"
})
Effect.runPromiseExit(program).then(console.log)
/*
输出:
Action called 1 time(s)
Action called 2 time(s)
Action called 3 time(s)
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Fail', failure: 'Error 3' }
}
*/
如果重试条件基于成功结果而非错误,也可以使用 Effect.repeat。
retryOrElse
Effect.retryOrElse 函数根据定义的调度策略多次重试失败的 effect。
若重试耗尽后 effect 仍失败,则运行备用 effect。
此函数适用于希望在多次失败后通过指定替代操作优雅处理失败的场景。
示例(带备选方案的重试)
import { Effect, Schedule, Console } from "effect"
let count = 0
// 模拟可能失败的 effect
const task = Effect.async<string, Error>((resume) => {
if (count <= 2) {
count++
console.log("failure")
resume(Effect.fail(new Error()))
} else {
console.log("success")
resume(Effect.succeed("yay!"))
}
})
// 重试任务,重试间有延迟且最多重试 2 次
const policy = Schedule.addDelay(Schedule.recurs(2), () => "100 millis")
// 若所有重试均失败,运行备选 effect
const repeated = Effect.retryOrElse(
task,
policy,
// 备选方案
() => Console.log("orElse").pipe(Effect.as("default value"))
)
Effect.runPromise(repeated).then(console.log)
/*
输出:
failure
failure
failure
orElse
default value
*/
在编程中,处理可能需要一些时间才能完成的任务是很常见的。通常,我们希望对这些任务的等待时间设置一个限制。Effect.timeout 函数通过为操作设置时间约束来确保它不会无限期地运行。
超时
基本用法
timeout
Effect.timeout 函数使用 Duration 参数来为操作设置时间限制。如果操作超过这个限制,会触发一个 TimeoutException,表示发生了超时。
示例(设置超时)
在这里,任务在超时时间内完成,因此成功返回结果。
import { Effect } from "effect"
const task = Effect.gen(function* () {
console.log("开始处理...")
yield* Effect.sleep("2 seconds") // 模拟处理延迟
console.log("处理完成。")
return "结果"
})
// 为任务设置3秒超时
const timedEffect = task.pipe(Effect.timeout("3 seconds"))
// 输出将显示任务成功完成
// 因为它处于超时时间内
Effect.runPromiseExit(timedEffect).then(console.log)
/*
输出:
开始处理...
处理完成。
{ _id: 'Exit', _tag: 'Success', value: '结果' }
*/
如果操作超过指定的时间,会引发 TimeoutException:
import { Effect } from "effect"
const task = Effect.gen(function* () {
console.log("开始处理...")
yield* Effect.sleep("2 seconds") // 模拟处理延迟
console.log("处理完成。")
return "结果"
})
// 输出将显示一个 TimeoutException,因为任务耗时
// 超过了指定的超时时间
const timedEffect = task.pipe(Effect.timeout("1 second"))
Effect.runPromiseExit(timedEffect).then(console.log)
/*
输出:
开始处理...
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Fail',
failure: { _tag: 'TimeoutException' }
}
}
*/
timeoutOption
如果你想更优雅地处理超时,可以考虑使用 Effect.timeoutOption。这个函数将超时视为常规结果,并将结果包装在 Option 中。
示例(将超时作为 Option 处理)
在这个例子中,第一个任务成功完成,而第二个任务超时。超时任务的结果以 Option 类型的 None 表示。
import { Effect } from "effect"
const task = Effect.gen(function* () {
console.log("开始处理...")
yield* Effect.sleep("2 seconds") // 模拟处理延迟
console.log("处理完成。")
return "结果"
})
const timedOutEffect = Effect.all([
task.pipe(Effect.timeoutOption("3 seconds")),
task.pipe(Effect.timeoutOption("1 second"))
])
Effect.runPromise(timedOutEffect).then(console.log)
/*
输出:
开始处理...
处理完成。
开始处理...
[
{ _id: 'Option', _tag: 'Some', value: '结果' },
{ _id: 'Option', _tag: 'None' }
]
*/
处理超时
当操作未在指定时间内完成时,Effect.timeout 的行为取决于操作是否是“不可中断的”。
不可中断的效果是指一旦开始,就不能被超时机制直接中途停止。 这可能是因为效果内的操作需要运行完成以避免系统处于不一致状态。
-
可中断操作:如果操作可以被中断,一旦达到超时阈值,它会立即终止,并产生一个
TimeoutException。import { Effect } from "effect" const task = Effect.gen(function* () { console.log("开始处理...") yield* Effect.sleep("2 seconds") // 模拟处理延迟 console.log("处理完成。") return "结果" }) const timedEffect = task.pipe(Effect.timeout("1 second")) Effect.runPromiseExit(timedEffect).then(console.log) /* 输出: 开始处理... { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: { _tag: 'TimeoutException' } } } */ -
不可中断操作:如果操作是不可中断的,它会继续运行直到完成,然后才会评估
TimeoutException。import { Effect } from "effect" const task = Effect.gen(function* () { console.log("开始处理...") yield* Effect.sleep("2 seconds") // 模拟处理延迟 console.log("处理完成。") return "结果" }) const timedEffect = task.pipe( Effect.uninterruptible, Effect.timeout("1 second") ) // 在任务完成后输出 TimeoutException, // 因为任务不可中断 Effect.runPromiseExit(timedEffect).then(console.log) /* 输出: 开始处理... 处理完成。 { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: { _tag: 'TimeoutException' } } } */
超时断开连接
Effect.disconnect 函数提供了一种更灵活的方式来处理不可中断效果中的超时。它允许不可中断的效果在后台完成,而主控制流则像发生了超时一样继续执行。
以下是区别:
不使用 Effect.disconnect:
- 不可中断的效果会忽略超时并继续执行直到完成,之后才会评估超时错误。
- 这可能导致识别超时条件的延迟,因为系统必须等待效果完成。
使用 Effect.disconnect:
- 不可中断的效果被允许在后台独立于主控制流继续运行。
- 主控制流立即识别超时并继续处理超时错误或替代逻辑,无需等待效果完成。
- 这种方法特别适用于效果的操不需要阻塞程序继续运行的情况,尽管被标记为不可中断。
示例(运行带超时和后台完成的不可中断任务)
考虑一个场景,启动了一个长时间运行的数据处理任务,你希望确保即使数据处理耗时过长,系统也能保持响应:
import { Effect } from "effect"
const longRunningTask = Effect.gen(function* () {
console.log("开始繁重处理...")
yield* Effect.sleep("5 seconds") // 模拟长时间处理
console.log("繁重处理完成。")
return "数据处理完成"
})
const timedEffect = longRunningTask.pipe(
Effect.uninterruptible,
// 允许任务在超时后在后台完成
Effect.disconnect,
Effect.timeout("1 second")
)
Effect.runPromiseExit(timedEffect).then(console.log)
/*
输出:
开始繁重处理...
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Fail',
failure: { _tag: 'TimeoutException' }
}
}
繁重处理完成。
*/
在这个例子中,系统在一秒后检测到超时,但长时间运行的任务在后台继续并完成,不会阻塞程序流程。
自定义超时行为
除了基本的 Effect.timeout 函数外,还有一些变体可以让你自定义超时发生时的行为。
timeoutFail
Effect.timeoutFail 函数允许你在超时发生时产生特定的错误。
示例(自定义超时错误)
import { Effect } from "effect"
const task = Effect.gen(function* () {
console.log("开始处理...")
yield* Effect.sleep("2 seconds") // 模拟处理延迟
console.log("处理完成。")
return "结果"
})
class MyTimeoutError {
readonly _tag = "MyTimeoutError"
}
const program = task.pipe(
Effect.timeoutFail({
duration: "1 second",
onTimeout: () => new MyTimeoutError() // 自定义超时错误
})
)
Effect.runPromiseExit(program).then(console.log)
/*
输出:
开始处理...
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Fail',
failure: MyTimeoutError { _tag: 'MyTimeoutError' }
}
}
*/
timeoutFailCause
Effect.timeoutFailCause 允许你定义在超时发生时抛出的特定缺陷。这有助于将超时视为代码中的异常情况。
示例(超时时的自定义缺陷)
import { Effect, Cause } from "effect"
const task = Effect.gen(function* () {
console.log("开始处理...")
yield* Effect.sleep("2 seconds") // 模拟处理延迟
console.log("处理完成。")
return "结果"
})
const program = task.pipe(
Effect.timeoutFailCause({
duration: "1 second",
onTimeout: () => Cause.die("超时了!") // 超时的自定义缺陷
})
)
Effect.runPromiseExit(program).then(console.log)
/*
输出:
开始处理...
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Die', defect: '超时了!' }
}
*/
timeoutTo
与 Effect.timeout 相比,Effect.timeoutTo 提供了更大的灵活性,允许你为成功和超时的操作定义不同的结果。这在你想根据操作是否按时完成来自定义结果时非常有用。
示例(使用 Either 处理成功和超时)
import { Effect, Either } from "effect"
const task = Effect.gen(function* () {
console.log("开始处理...")
yield* Effect.sleep("2 seconds") // 模拟处理延迟
console.log("处理完成。")
return "结果"
})
const program = task.pipe(
Effect.timeoutTo({
duration: "1 second",
onSuccess: (result): Either.Either<string, string> =>
Either.right(result),
onTimeout: (): Either.Either<string, string> =>
Either.left("超时了!")
})
)
Effect.runPromise(program).then(console.log)
/*
输出:
开始处理...
{
_id: "Either",
_tag: "Left",
left: "超时了!"
}
*/
沙盒化
错误是编程中不可避免的一部分,它们可能来自各种源头,如故障、缺陷、纤程中断或这些因素的组合。本指南将介绍如何使用Effect.sandbox函数来隔离和理解基于Effect的代码中错误的成因。
sandbox / unsandbox
Effect.sandbox函数允许你将一个effect中所有潜在的错误成因封装起来。它会暴露effect的完整错误成因,无论是由故障、缺陷、纤程中断还是这些因素的组合所导致。
简单来说,它接收一个Effect<A, E, R>类型的effect,并将其转换为Effect<A, Cause<E>, R>类型的effect,其中错误通道现在包含了详细的错误成因。
语法
Effect<A, E, R> -> Effect<A, Cause<E>, R>
通过使用Effect.sandbox函数,你可以访问异常effect的底层成因。这些成因以Cause<E>类型表示,并出现在Effect数据类型的错误通道中。
一旦暴露了这些成因,你就可以利用标准的错误处理操作符,如Effect.catchAll和Effect.catchTags,来更有效地处理错误。这些操作符允许你对特定的错误条件做出响应。
如果需要,我们可以用Effect.unsandbox撤销沙盒化操作。
示例(处理不同的错误成因)
import { Effect, Console } from "effect"
// ┌─── Effect<string, Error, never>
// ▼
const task = Effect.fail(new Error("出错了!")).pipe(
Effect.as("主要结果")
)
// ┌─── Effect<string, Cause<Error>, never>
// ▼
const sandboxed = Effect.sandbox(task)
const program = Effect.catchTags(sandboxed, {
Die: (cause) =>
Console.log(`捕获到缺陷: ${cause.defect}`).pipe(
Effect.as("缺陷时的备用结果")
),
Interrupt: (cause) =>
Console.log(`捕获到缺陷: ${cause.fiberId}`).pipe(
Effect.as("纤程中断时的备用结果")
),
Fail: (cause) =>
Console.log(`捕获到缺陷: ${cause.error}`).pipe(
Effect.as("故障时的备用结果")
)
})
// 使用unsandbox恢复原始的错误处理
const main = Effect.unsandbox(program)
Effect.runPromise(main).then(console.log)
/*
输出:
捕获到缺陷: 出错了!
故障时的备用结果
*/
错误累加
诸如 Effect.zip、Effect.all 和 Effect.forEach 等顺序组合器在错误管理上采用"快速失败"策略。这意味着它们在遇到第一个错误时会立即停止并返回。
以下是一个使用 Effect.zip 的示例,它在第一次失败时停止,并仅显示第一个错误:
示例(Effect.zip 的快速失败)
import { Effect, Console } from "effect"
const task1 = Console.log("task1").pipe(Effect.as(1))
const task2 = Effect.fail("Oh uh!").pipe(Effect.as(2))
const task3 = Console.log("task2").pipe(Effect.as(3))
const task4 = Effect.fail("Oh no!").pipe(Effect.as(4))
const program = task1.pipe(
Effect.zip(task2),
Effect.zip(task3),
Effect.zip(task4)
)
Effect.runPromise(program).then(console.log, console.error)
/*
输出:
task1
(FiberFailure) Error: Oh uh!
*/
Effect.forEach 函数的行为类似。它对集合中的每个元素应用一个有效操作,但会在遇到第一个错误时停止:
示例(Effect.forEach 的快速失败)
import { Effect, Console } from "effect"
const program = Effect.forEach([1, 2, 3, 4, 5], (n) => {
if (n < 4) {
return Console.log(`item ${n}`).pipe(Effect.as(n))
} else {
return Effect.fail(`${n} is not less that 4`)
}
})
Effect.runPromise(program).then(console.log, console.error)
/*
输出:
item 1
item 2
item 3
(FiberFailure) Error: 4 is not less that 4
*/
然而,在某些情况下,您可能希望收集所有错误而不是快速失败。在这些情况下,可以使用累积成功和错误的函数。
validate
Effect.validate 函数类似于 Effect.zip,但它在遇到错误后仍会继续组合效果,累积成功和失败。
示例(验证并收集错误)
import { Effect, Console } from "effect"
const task1 = Console.log("task1").pipe(Effect.as(1))
const task2 = Effect.fail("Oh uh!").pipe(Effect.as(2))
const task3 = Console.log("task2").pipe(Effect.as(3))
const task4 = Effect.fail("Oh no!").pipe(Effect.as(4))
const program = task1.pipe(
Effect.validate(task2),
Effect.validate(task3),
Effect.validate(task4)
)
Effect.runPromiseExit(program).then(console.log)
/*
输出:
task1
task2
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Sequential',
left: { _id: 'Cause', _tag: 'Fail', failure: 'Oh uh!' },
right: { _id: 'Cause', _tag: 'Fail', failure: 'Oh no!' }
}
}
*/
validateAll
Effect.validateAll 函数类似于 Effect.forEach 函数。它使用提供的有效操作转换集合中的所有元素,但会在错误通道中收集所有错误,并在成功通道中收集所有成功值。
import { Effect, Console } from "effect"
// ┌─── Effect<number[], string[], never>
// ▼
const program = Effect.validateAll([1, 2, 3, 4, 5], (n) => {
if (n < 4) {
return Console.log(`item ${n}`).pipe(Effect.as(n))
} else {
return Effect.fail(`${n} is not less that 4`)
}
})
Effect.runPromiseExit(program).then(console.log)
/*
输出:
item 1
item 2
item 3
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Fail',
failure: [ '4 is not less that 4', '5 is not less that 4' ]
}
}
*/
请注意,此函数是有损的,这意味着如果存在任何错误,所有成功值都将丢失。如果需要同时保留成功和失败,请考虑使用 Effect.partition。
validateFirst
Effect.validateFirst 函数类似于 Effect.validateAll,但它返回第一个成功的结果,如果没有成功则返回所有错误。
示例(返回第一个成功)
import { Effect, Console } from "effect"
// ┌─── Effect<number, string[], never>
// ▼
const program = Effect.validateFirst([1, 2, 3, 4, 5], (n) => {
if (n < 4) {
return Effect.fail(`${n} is not less that 4`)
} else {
return Console.log(`item ${n}`).pipe(Effect.as(n))
}
})
Effect.runPromise(program).then(console.log, console.error)
/*
输出:
item 4
4
*/
注意,Effect.validateFirst 返回一个单一的 number 作为成功类型,而不是像 Effect.validateAll 那样返回一个结果数组。
partition
Effect.partition 函数处理一个可迭代对象,并对每个元素应用一个有效函数。它返回一个元组,其中第一部分包含所有失败,第二部分包含所有成功。
示例(分区成功和失败)
import { Effect } from "effect"
// ┌─── Effect<[string[], number[]], never, never>
// ▼
const program = Effect.partition([0, 1, 2, 3, 4], (n) => {
if (n % 2 === 0) {
return Effect.succeed(n)
} else {
return Effect.fail(`${n} is not even`)
}
})
Effect.runPromise(program).then(console.log, console.error)
/*
输出:
[ [ '1 is not even', '3 is not even' ], [ 0, 2, 4 ] ]
*/
此操作符是一个非异常效果,意味着错误通道类型为 never。失败会被收集而不会停止效果,因此整个操作会完成并返回错误和成功。
错误通道操作
实际上,你可以对效果的错误通道执行各种操作。这些操作允许你以不同的方式转换、检查和处理错误。让我们来探讨其中一些操作。
映射操作
mapError
Effect.mapError 函数用于当你需要转换或修改效果产生的错误,而不影响成功值时。这在你想为错误添加额外信息或更改其类型时非常有用。
示例(映射错误)
这里,错误类型从 string 变为 Error。
import { Effect } from "effect"
// ┌─── Effect<number, string, never>
// ▼
const simulatedTask = Effect.fail("Oh no!").pipe(Effect.as(1))
// ┌─── Effect<number, Error, never>
// ▼
const mapped = Effect.mapError(
simulatedTask,
(message) => new Error(message)
)
需要注意的是,使用
Effect.mapError函数不会改变效果的总体成功或失败状态。它只转换错误通道中的值,同时保留效果的原始成功或失败状态。
mapBoth
Effect.mapBoth 函数允许你对两个通道应用转换:效果的错误通道和成功通道。它接受两个映射函数作为参数:一个用于错误通道,另一个用于成功通道。
示例(映射成功和错误)
import { Effect } from "effect"
// ┌─── Effect<number, string, never>
// ▼
const simulatedTask = Effect.fail("Oh no!").pipe(Effect.as(1))
// ┌─── Effect<boolean, Error, never>
// ▼
const modified = Effect.mapBoth(simulatedTask, {
onFailure: (message) => new Error(message),
onSuccess: (n) => n > 0
})
需要注意的是,使用 Effect.mapBoth 函数不会改变效果的总体成功或失败状态。它只转换错误和成功通道中的值,同时保留效果的原始成功或失败状态。
过滤成功通道
Effect 库提供了几个操作符,用于根据给定的谓词过滤成功通道中的值。
这些操作符为谓词失败的情况提供了不同的处理策略:
| API | 描述 |
|---|---|
filterOrFail | 此操作符根据谓词过滤成功通道中的值。如果谓词对任何值失败,原始效果会失败并返回错误。 |
filterOrDie / filterOrDieMessage | 这些操作符也根据谓词过滤成功通道中的值。如果谓词对任何值失败,原始效果会突然终止。filterOrDieMessage 变体允许你提供自定义错误消息。 |
filterOrElse | 此操作符根据谓词过滤成功通道中的值。如果谓词对任何值失败,则执行替代效果。 |
示例(过滤成功值)
import { Effect, Random, Cause } from "effect"
// 如果谓词为 false,则失败并返回自定义错误
const task1 = Effect.filterOrFail(
Random.nextRange(-1, 1),
(n) => n >= 0,
() => "随机数为负数"
)
// 如果谓词为 false,则突然终止并返回自定义异常
const task2 = Effect.filterOrDie(
Random.nextRange(-1, 1),
(n) => n >= 0,
() => new Cause.IllegalArgumentException("随机数为负数")
)
// 如果谓词为 false,则突然终止并返回自定义错误消息
const task3 = Effect.filterOrDieMessage(
Random.nextRange(-1, 1),
(n) => n >= 0,
"随机数为负数"
)
// 如果谓词为 false,则执行替代效果
const task4 = Effect.filterOrElse(
Random.nextRange(-1, 1),
(n) => n >= 0,
() => task3
)
需要注意的是,根据使用的特定过滤操作符,效果可以在谓词失败时失败、突然终止或执行替代效果。根据你想要的错误处理策略和程序逻辑选择合适的操作符。
过滤 API 还可以与用户定义的类型守卫结合使用,以提高类型安全性和代码清晰度。这确保只有有效类型才能通过。
示例(使用类型守卫)
import { Effect, pipe } from "effect"
// 定义用户接口
interface User {
readonly name: string
}
// 模拟异步认证函数
declare const auth: () => Promise<User | null>
const program = pipe(
Effect.promise(() => auth()),
// 使用 filterOrFail 和自定义类型守卫确保用户不为 null
Effect.filterOrFail(
(user): user is User => user !== null, // 类型守卫
() => new Error("未授权")
),
// 'user' 现在具有 `User` 类型(而不是 `User | null`)
Effect.andThen((user) => user.name)
)
在上面的示例中,filterOrFail API 中使用了一个守卫来确保 user 是 User 类型而不是 User | null。
如果你愿意,可以使用预制的守卫如 Predicate.isNotNull 来简化并保持一致性。
检查错误
类似于成功值的 tapping,Effect 提供了几个操作符来检查错误值。这些操作符允许开发者观察失败或潜在问题而不改变结果。
tapError
执行一个效果操作来检查效果的失败,而不改变它。
示例(检查错误)
import { Effect, Console } from "effect"
// 模拟一个失败的任务
const task: Effect.Effect<number, string> = Effect.fail("网络错误")
// 使用 tapError 在任务失败时记录错误消息
const tapping = Effect.tapError(task, (error) =>
Console.log(`预期错误: ${error}`)
)
Effect.runFork(tapping)
/*
输出:
预期错误: 网络错误
*/
tapErrorTag
此函数允许你检查匹配特定标签的错误,帮助你更精确地处理不同类型的错误。
示例(检查标记错误)
import { Effect, Console, Data } from "effect"
class NetworkError extends Data.TaggedError("NetworkError")<{
readonly statusCode: number
}> {}
class ValidationError extends Data.TaggedError("ValidationError")<{
readonly field: string
}> {}
// 创建一个失败的任务,返回 NetworkError
const task: Effect.Effect<number, NetworkError | ValidationError> =
Effect.fail(new NetworkError({ statusCode: 504 }))
// 使用 tapErrorTag 仅检查 NetworkError 类型
// 并记录状态码
const tapping = Effect.tapErrorTag(task, "NetworkError", (error) =>
Console.log(`预期错误: ${error.statusCode}`)
)
Effect.runFork(tapping)
/*
输出:
预期错误: 504
*/
tapErrorCause
此函数用于检查错误的完整原因,包括失败和缺陷。
示例(检查错误原因)
import { Effect, Console } from "effect"
// 创建一个因 NetworkError 失败的任务
const task1: Effect.Effect<number, string> = Effect.fail("NetworkError")
const tapping1 = Effect.tapErrorCause(task1, (cause) =>
Console.log(`错误原因: ${cause}`)
)
Effect.runFork(tapping1)
/*
输出:
错误原因: Error: NetworkError
*/
// 模拟系统中的严重故障
const task2: Effect.Effect<number, string> = Effect.dieMessage(
"出错了"
)
const tapping2 = Effect.tapErrorCause(task2, (cause) =>
Console.log(`错误原因: ${cause}`)
)
Effect.runFork(tapping2)
/*
输出:
错误原因: RuntimeException: 出错了
... 堆栈跟踪 ...
*/
tapDefect
专门检查 effect 中不可恢复的失败或缺陷(即一个或多个 Die 原因)。
示例(检查缺陷)
import { Effect, Console } from "effect"
// 模拟一个因可恢复错误失败的任务
const task1: Effect.Effect<number, string> = Effect.fail("NetworkError")
// tapDefect 不会记录任何内容,因为 NetworkError 不是缺陷
const tapping1 = Effect.tapDefect(task1, (cause) =>
Console.log(`缺陷: ${cause}`)
)
Effect.runFork(tapping1)
/*
无输出
*/
// 模拟系统中的严重故障
const task2: Effect.Effect<number, string> = Effect.dieMessage(
"出错了"
)
// 使用 tapDefect 记录缺陷
const tapping2 = Effect.tapDefect(task2, (cause) =>
Console.log(`缺陷: ${cause}`)
)
Effect.runFork(tapping2)
/*
输出:
缺陷: RuntimeException: 出错了
... 堆栈跟踪 ...
*/
tapBoth
检查 effect 的成功和失败结果,根据结果执行不同的操作。
示例(检查成功和失败)
import { Effect, Random, Console } from "effect"
// 模拟一个可能失败的任务
const task = Effect.filterOrFail(
Random.nextRange(-1, 1),
(n) => n >= 0,
() => "随机数为负数"
)
// 使用 tapBoth 记录成功和失败结果
const tapping = Effect.tapBoth(task, {
onFailure: (error) => Console.log(`失败: ${error}`),
onSuccess: (randomNumber) =>
Console.log(`随机数: ${randomNumber}`)
)
Effect.runFork(tapping)
/*
示例输出:
失败: 随机数为负数
*/
在成功通道中暴露错误
Effect.either 函数将 Effect<A, E, R> 转换为一个 effect,该 effect 将潜在的失败和成功封装在 Either 数据类型中:
Effect<A, E, R> -> Effect<Either<A, E>, never, R>
这意味着如果你有一个以下类型的 effect:
Effect<string, HttpError, never>
并对其调用 Effect.either,类型将变为:
Effect<Either<string, HttpError>, never, never>
生成的 effect 不会失败,因为潜在的失败现在由 Either 的 Left 类型表示。
返回的 Effect 的错误类型指定为 never,确认该 effect 的结构不会失败。
在使用 Effect.gen 时,此函数特别有用,可以从可能失败的 effect 中恢复:
示例(使用 Effect.either 处理错误)
import { Effect, Either, Console } from "effect"
// 模拟一个失败的任务
//
// ┌─── Either<number, string, never>
// ▼
const program = Effect.fail("出错了!").pipe(Effect.as(2))
// ┌─── Either<number, never, never>
// ▼
const recovered = Effect.gen(function* () {
// ┌─── Either<number, string>
// ▼
const failureOrSuccess = yield* Effect.either(program)
if (Either.isLeft(failureOrSuccess)) {
const error = failureOrSuccess.left
yield* Console.log(`失败: ${error}`)
return 0
} else {
const value = failureOrSuccess.right
yield* Console.log(`成功: ${value}`)
return value
}
})
Effect.runPromise(recovered).then(console.log)
/*
输出:
失败: 出错了!
0
*/
在成功通道中暴露原因
你可以使用 Effect.cause 函数来暴露 effect 的原因,这是对失败的更详细表示,包括错误消息和缺陷。
示例(记录失败原因)
import { Effect, Console } from "effect"
// ┌─── Effect<number, string, never>
// ▼
const program = Effect.fail("出错了!").pipe(Effect.as(2))
// ┌─── Effect<void, never, never>
// ▼
const recovered = Effect.gen(function* () {
const cause = yield* Effect.cause(program)
yield* Console.log(cause)
})
将错误通道合并到成功通道
Effect.merge 函数允许你将错误通道与成功通道合并。这会产生一个永远不会失败的 effect;相反,成功和错误都作为成功通道中的值处理。
示例(合并错误和成功通道)
import { Effect } from "effect"
// ┌─── Effect<number, string, never>
// ▼
const program = Effect.fail("出错了!").pipe(Effect.as(2))
// ┌─── Effect<number | string, never, never>
// ▼
const recovered = Effect.merge(program)
交换错误和成功通道
Effect.flip 函数允许你交换 effect 的错误和成功通道。这意味着之前的成功变为错误,反之亦然。
示例(交换错误和成功通道)
import { Effect } from "effect"
// ┌─── Effect<number, string, never>
// ▼
const program = Effect.fail("出错了!").pipe(Effect.as(2))
// ┌─── Effect<string, number, never>
// ▼
const flipped = Effect.flip(program)
并行和顺序错误
在使用 Effect 时,如果发生错误,默认行为是在遇到第一个错误时失败。
示例(在第一个错误时失败)
这里,程序在遇到第一个错误 "Oh uh!" 时失败。
import { Effect } from "effect"
const fail = Effect.fail("Oh uh!")
const die = Effect.dieMessage("Boom!")
// 依次运行两个效果
const program = Effect.all([fail, die])
Effect.runPromiseExit(program).then(console.log)
/*
输出:
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Fail', failure: 'Oh uh!' }
}
*/
并行错误
在某些情况下,你可能会遇到多个错误,特别是在并发计算中。当任务并发运行时,多个错误可能同时发生。
示例(处理并发计算中的多个错误)
在这个例子中,fail 和 die 效果是并发执行的。由于两者都失败了,程序会在输出中报告多个错误。
import { Effect } from "effect"
const fail = Effect.fail("Oh uh!")
const die = Effect.dieMessage("Boom!")
// 并发运行两个效果
const program = Effect.all([fail, die], {
concurrency: "unbounded"
}).pipe(Effect.asVoid)
Effect.runPromiseExit(program).then(console.log)
/*
输出:
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Parallel',
left: { _id: 'Cause', _tag: 'Fail', failure: 'Oh uh!' },
right: { _id: 'Cause', _tag: 'Die', defect: [Object] }
}
}
*/
parallelErrors
Effect 提供了一个名为 Effect.parallelErrors 的函数,可以在错误通道中捕获并发操作中的所有失败错误。
示例(捕获多个并发失败)
在这个例子中,Effect.parallelErrors 将 fail1 和 fail2 的错误合并为一个错误。
import { Effect } from "effect"
const fail1 = Effect.fail("Oh uh!")
const fail2 = Effect.fail("Oh no!")
const die = Effect.dieMessage("Boom!")
// 并发运行所有效果并捕获所有错误
const program = Effect.all([fail1, fail2, die], {
concurrency: "unbounded"
}).pipe(Effect.asVoid, Effect.parallelErrors)
Effect.runPromiseExit(program).then(console.log)
/*
输出:
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Fail', failure: [ 'Oh uh!', 'Oh no!' ] }
}
*/
注意 Effect.parallelErrors 仅适用于失败(failures),不适用于缺陷(defects)或中断(interruptions)。
顺序错误
当使用资源安全操作符如 Effect.ensuring 时,你可能会遇到多个顺序错误。
这是因为无论原始效果是否有任何错误,终结器都是不可中断的,并且总是会运行。
示例(处理多个顺序错误)
在这个例子中,fail 和终结器 die 都导致了顺序错误,两者都被捕获。
import { Effect } from "effect"
// 模拟一个失败的效果
const fail = Effect.fail("Oh uh!")
// 模拟一个导致缺陷的终结器
const die = Effect.dieMessage("Boom!")
// 即使 'fail' 失败,终结器 'die' 也会始终运行
const program = fail.pipe(Effect.ensuring(die))
Effect.runPromiseExit(program).then(console.log)
/*
输出:
{
_id: 'Exit',
_tag: 'Failure',
cause: {
_id: 'Cause',
_tag: 'Sequential',
left: { _id: 'Cause', _tag: 'Fail', failure: 'Oh uh!' },
right: { _id: 'Cause', _tag: 'Die', defect: [Object] }
}
}
*/
可生成错误
可生成错误(Yieldable Errors)是一种特殊类型的错误,可以直接在生成器函数中使用 Effect.gen 来生成。这类错误允许您以直观的方式处理,无需显式调用 Effect.fail,从而简化了代码中自定义错误的管理方式。
Data.Error
Data.Error 构造函数提供了一种定义可生成错误基类的方式。
示例(创建并生成自定义错误)
import { Effect, Data } from "effect"
// 定义一个继承自 Data.Error 的自定义错误类
class MyError extends Data.Error<{ message: string }> {}
export const program = Effect.gen(function* () {
// 生成自定义错误(等同于使用 MyError 失败)
yield* new MyError({ message: "糟糕!" })
})
Effect.runPromiseExit(program).then(console.log)
/*
输出:
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Fail', failure: { message: '糟糕!' } }
}
*/
Data.TaggedError
Data.TaggedError 构造函数允许您定义带有唯一标签的可生成错误。每个错误都有一个 _tag 属性,便于区分不同类型的错误。这使得使用 Effect.catchTag 或 Effect.catchTags 等函数处理特定标签错误变得非常方便。
示例(处理多个带标签的错误)
import { Effect, Data, Random } from "effect"
// 带有 _tag: "Foo" 的错误
class FooError extends Data.TaggedError("Foo")<{
message: string
}> {}
// 带有 _tag: "Bar" 的错误
class BarError extends Data.TaggedError("Bar")<{
randomNumber: number
}> {}
const program = Effect.gen(function* () {
const n = yield* Random.next
return n > 0.5
? "成功!"
: n < 0.2
? yield* new FooError({ message: "糟糕!" })
: yield* new BarError({ randomNumber: n })
}).pipe(
// 使用 catchTags 处理不同的标签错误
Effect.catchTags({
Foo: (error) => Effect.succeed(`Foo 错误:${error.message}`),
Bar: (error) => Effect.succeed(`Bar 错误:${error.randomNumber}`)
})
)
Effect.runPromise(program).then(console.log, console.error)
/*
示例输出(n < 0.2):
Foo 错误:糟糕!
*/