怎么用 catchReasons 处理 HttpClientError?

一次 HTTP 调用失败,可能是 404,可能是 500,也可能是根本没连上,而这三种在 HttpClientError 里是同一个外层错误,区别藏在它的 reason 里。

对 HttpClientError 用 Effect.catchReasons("HttpClientError", { StatusCodeError: ..., TransportError: ... }),按 reason 的 _tag 分别恢复,没写的 reason 保持失败。

HttpClientError 是 effect/http 里典型的「父错误 + tagged reason」形状,reason 包括 TransportError、StatusCodeError、DecodeError 等。这个错误在 effect 4.0.5 里标着 @stability unstable。catchReasons 的通用用法见Effect 怎么处理「父错误 + 嵌套 reason」的两级错误,下面只看 HTTP 这个具体场景。

怎么把 404 当作不存在、传输失败当作离线?

下面的例子先造一个能按状态码失败的 respond,再用 catchReasons 恢复:

import { Effect } from "effect"
import { HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http"

const request = HttpClientRequest.get("http://localhost/users/1")

const respond = (status: number): Effect.Effect<string, HttpClientError.HttpClientError> =>
  status === 200
    ? Effect.succeed("ok")
    : Effect.fail(
        new HttpClientError.HttpClientError({
          reason: new HttpClientError.StatusCodeError({
            request,
            response: HttpClientResponse.fromWeb(request, new Response(null, { status }))
          })
        })
      )

export const program = (status: number) =>
  respond(status).pipe(
    Effect.catchReasons("HttpClientError", {
      StatusCodeError: (r) => (r.response.status === 404 ? Effect.succeed("missing") : Effect.fail(r)),
      TransportError: () => Effect.succeed("offline")
    })
  )

我实际跑了 200、404、500 和一个 TransportError:分别得到 ok、missing、StatusCodeError 失败、offline。

处理器里 Effect.fail(r) 失败出去的是什么?

失败出去的是 reason(StatusCodeError)本身,而不是原来的 HttpClientError。处理器的失败类型会直接进入结果的 E,和父错误是否保留互不相干。所以 500 那个分支,调用方在 E 里看到的是 StatusCodeError。

另外,这个例子没有带兜底,父错误仍然留在 E 里,原因和解法见catchReasons 全部写完,为什么类型里还有父错误?。

AI 模块的 AiError 也能这样处理吗?

可以。AiError.AiError 的 reason 是 RateLimitError、QuotaExhaustedError、ContentPolicyError、InvalidRequestError、NetworkError 等十几个变体。我用 AiError.make 构造了一个限流错误,catchReason("AiError", "RateLimitError", ...) 和 unwrapReason("AiError") 都能通过 tsc 并给出正确的类型。读 retryAfter 时要注意它在 reason 上是可选的 Duration。

用在生产代码里要注意什么?

HttpClientError 和 AiError 本身是 unstable,字段和 reason 集合在后续版本可能调整,升级版本后要重新确认 reason 的 tag 名;catchReason 这一组组合子是 stable。reason 的 tag 写错会直接编译失败,这是升级时能提前发现变化的原因。

版本是 effect 4.0.5。本文的 HTTP 例子在这个版本上运行过 200、404、500 和 TransportError 四种输入;AiError 部分只用 tsc 检查过类型,没有运行。HttpClientError 与 AiError 属于 unstable,后续版本可能变化。