Effect 里 timeout 和 retry 谁包着谁?

Promise.race([请求, 定时器]) 超时之后,输掉的那个请求并没有被取消,它依然在后台占着连接和内存。

在 Effect 里,timeout 在 retry 里面,限制的是每一次尝试;timeout 在 retry 外面,限制的是总预算。 位置不同语义不同,没有对错,只看你想表达什么。

下面先说 Effect.timeout 超时时到底发生了什么,再讲两种嵌套,最后讲一个容易被忽略的细节:中断能不能真的传到底层请求。重试策略本身怎么写,见 Effect 里怎么给偶发失败的调用加重试。

Effect.timeout 超时时会发生什么?

Effect.timeout(duration) 让一个 Effect 在限定时间内没结束就以 TimeoutError 失败。它是类型里的失败,会出现在 E 里,所以可以被 catchTag 捕获,也可以被 retry 的 while 判断。

更重要的是:超时赢了之后,源 Effect 会被中断,而不是丢在后台。Effect 资源与 Scope 里的 finalizer 会被执行,这是它比 Promise.race 可靠的地方。

如果「超时」在你的业务里是正常分支而不是错误,用 Effect.timeoutOption,它在超时时返回 Option.none,不会让整个 Effect 失败。

timeout 放在 retry 里面还是外面?

// timeout 在里面:限制每一次尝试
effect.pipe(Effect.timeout("2 seconds"), Effect.retry(...))

// timeout 在外面:限制总预算
effect.pipe(Effect.retry(...), Effect.timeout("5 seconds"))
  • 里面:每次尝试各自最多等 2 秒。三次尝试最长共等六秒多。
  • 外面:不管重试几次,五秒一到整体失败。

面向用户的请求通常想要后者,总预算可预期;后台任务常常想要前者。以内层 timeout 加 retry({ times: 2 }) 为例,一个必然超时的操作会被执行三次,最终以 TimeoutError 失败。

超时一定能取消底层请求吗?

不一定,取决于底层能不能被取消。用 Effect.tryPromise 包装 fetch 时,回调会收到一个 AbortSignal,把它传给 fetch,超时中断才能真正取消网络请求。不传的话,Effect 这一侧已经停了,底层请求还在跑。

本文以 effect v4 为准,原文没有标注具体版本号。超时后源 Effect 被中断、内层与外层 timeout 的语义、「必然超时的操作执行三次」这几条是对 API 行为的描述,没有单独跑过;AbortSignal 的传递方式同样没有对真实网络请求验证,代码片段只是示意嵌套顺序,没有完整运行。