Effect v4 升级后有哪些编译器不会报的行为变化?
升级后 tsc 全绿,catch (e) 里的 e instanceof MyError 却从 false 变成了 true,手写的 _tag === "TimeoutException" 比较则悄悄永远不成立。
行为差异共有七处:runPromise 的拒绝值、Option 与 Result 不再是 Effect、超时错误改名、Stream.runCollect 返回数组、解码错误改名、Cause 变扁平、缺失配置的报错信息。其中编译器基本拦不住的,要靠测试。
每一条我都在 effect 3.22.3 与 4.0.5 上各跑了一次,对照确认。先看清单,再看一份能直接跑的测试。改名类的变化见 Effect 从 v3 升级到 v4 要改哪些地方?。
runPromise 失败时拒绝的是什么?
v3 里失败会被包成 FiberFailure,catch (e) 里 e instanceof MyError 是 false。v4 直接拒绝原始的错误对象,instanceof 成立;Effect.die("x") 则直接拒绝字符串 "x"。依赖旧行为的错误处理中间件要重新检查。
Option 和 Result 还能直接 yield* 吗?
不能。v3 里可以在 Effect.gen 里 yield* Option.some(1),yield* Either.left(...) 会让 Effect 失败;v4 里这既是类型错误,运行时也会报 Not a valid effect。显式转换:Effect.fromOption(缺值时以 NoSuchElementError 失败)与 Effect.fromResult。
超时、解码、Stream 和 Cause 变了什么?
- 超时错误改名。
Effect.timeout的失败从TimeoutException变成Cause.TimeoutError,_tag同步改为"TimeoutError"。类型化的catchTag("TimeoutException")会直接编译失败,但手写_tag === "TimeoutException"比较的地方会静默失效,需要全局搜一遍。 Stream.runCollect返回数组。v3 返回Chunk,v4 返回普通Array。所有对结果调用Chunk.toReadonlyArray的地方都要改。- 解码错误改名。
ParseError变成SchemaError,同步解码抛出的也是SchemaError。 - Cause 的形状。v3 的
Cause是一棵树(Sequential、Parallel),v4 是扁平的,通过cause.reasons拿到一个数组,元素_tag是Fail、Die、Interrupt。
缺失的配置会报什么错?
Config.Redacted("KEY") 缺失时,失败是 ConfigError,里面包着 SchemaError,信息是 Expected string at ["KEY"],变量名只出现在路径里,对运维不算友好,值得自己再包一层。Redacted 本身的用法见 Effect 的 Redacted 怎么防止密钥泄漏到日志?。
怎么用测试把这些行为固定下来?
下面这份测试覆盖了前面的大部分条目:
// behaviors.test.ts
import { it, expect } from "@effect/vitest"
import { Cause, Data, Effect, Fiber, Option, Result, Schema, Stream } from "effect"
import { TestClock } from "effect/testing"
class MyError extends Data.TaggedError("MyError")<{ readonly n: number }> {}
it("runPromise 拒绝原始错误对象", async () => {
const rejected = await Effect.runPromise(Effect.fail(new MyError({ n: 1 }))).catch((e: unknown) => e)
expect(rejected instanceof MyError).toBe(true)
})
it.effect("Option 与 Result 需要显式转换", () =>
Effect.gen(function* () {
const none = yield* Effect.flip(Effect.fromOption(Option.none<number>()))
expect(none._tag).toBe("NoSuchElementError")
const failed = yield* Effect.flip(Effect.fromResult(Result.fail("bad")))
expect(failed).toBe("bad")
})
)
it.effect("timeout 失败是 TimeoutError", () =>
Effect.gen(function* () {
const fiber = yield* Effect.forkChild(Effect.flip(Effect.timeout(Effect.never, "10 millis")))
yield* TestClock.adjust("10 millis")
const error = yield* Fiber.join(fiber)
expect(error._tag).toBe("TimeoutError")
})
)
it.effect("runCollect、SchemaError、Cause.reasons", () =>
Effect.gen(function* () {
const collected = yield* Stream.runCollect(Stream.make(1, 2, 3))
expect(Array.isArray(collected)).toBe(true)
const decodeError = yield* Effect.flip(Schema.decodeUnknownEffect(Schema.Number)("x"))
expect(decodeError._tag).toBe("SchemaError")
const cause = Cause.combine(Cause.fail("a"), Cause.die("b"))
expect(cause.reasons.map((r) => r._tag)).toEqual(["Fail", "Die"])
})
)版本:核对在 effect 4.0.5 上进行,每条行为都用 effect 3.22.3 对照过;测试用 @effect/vitest 实际运行。