Effect.promise 和 Effect.tryPromise 该用哪个?

fetch 会 reject,数据库驱动会 reject,几乎所有 I/O 都会 reject。但把它们包成 Effect 的时候,有一个构造函数默认你不在乎这件事。

答案是:绝大多数情况用 Effect.tryPromise,写完整形式,自己翻译错误。 Effect.promise 只在你确信这个 Promise 不会 reject 时才用,因为一旦 reject,它会被当作缺陷,而不是出现在 E 里的失败。

原因在于 Effect 把「预期的失败」和「不该发生的缺陷」分开处理,包装 Promise 时你必须表态。下面讲两者的区别、简写形式的问题,以及 signal 到底能做什么。如果你想先了解整组构造函数怎么选,见 怎么把已有的同步、Promise 代码包装成 Effect?

Effect.promise 适合什么场景?

const delayed = Effect.promise(() => Promise.resolve(1))

只在你确信这个 Promise 不会 reject 时使用。如果它 reject 了,会被当作缺陷,而不是失败。这类场景比想象中少,大多数 I/O 都可能失败。

所以把 Effect.promise 当成 Effect.tryPromise 的省事版本,是个常见的错误:它把所有 reject 变成缺陷,真实的失败从此无法被类型化处理。

Effect.tryPromise 的两种写法有什么区别?

// 简写:失败类型是 Cause.UnknownError
const w3 = Effect.tryPromise(() => fetch("https://api.example.com"))

// 完整写法:自己翻译错误
const w4 = Effect.tryPromise({
  try: (signal) => fetch("https://api.example.com", { signal }),
  catch: (cause) => new NetworkError({ cause })
})

传一个函数的简写形式,失败类型是 Cause.UnknownError(4.0.5 里的行为)。它只是装下了原始异常,没有任何语义,调用者无法据此分辨具体问题。

在应用代码里更推荐写完整形式,把失败翻译成自己的错误类型,这样调用者才能按类型处理。为什么是「翻译」而不是「透传」,见 错误设计。

另外,catch 应当 return 错误值,而不是 throw。如果它抛出,抛出的值会被当作缺陷。

signal 能让请求真的取消吗?

完整形式里,try 函数接收一个 signal,这是 AbortSignal。当这个 Effect 被中断时,signal 会被 abort。 把它传给 fetch,就把取消「接上」了。反过来,如果你不传,Effect 被中断时底层请求还会继续跑,只是结果被丢弃。

但传了 signal 不等于一定取消。signal 被 abort 之后,底层操作是否真的停下,取决于它有没有认真观察这个 signal。fetch 会,很多第三方 SDK 不会。

把 fetch 的结果直接传给 Effect.succeed 行不行?

不行。Effect.succeed(fetch(url)) 在构造时就把请求发出去了,是惰性的反面。始终用 () => ... 的形式,让 Effect 在运行时才调用它。

本文依据 effect 4.0.5 的行为整理,Cause.UnknownError 这一失败类型特指该版本。示例代码没有在本地逐条运行验证;第三方 SDK 是否观察 signal,需要按具体 SDK 自行确认。