# Resilience & Client Combinators

Real networks fail. The HTTP client treats resilience as middleware you layer
onto a client once, rather than logic you sprinkle across call sites. Because a
configured client is just a value, you compose retries, timeouts, status checks,
and custom request/response transforms with `.pipe`, and every request the
client sends inherits them. The result is a single place that defines how your
service behaves under load and partial failure.

```ts
import { Context, Effect, flow, Layer, Schedule, Schema } from "effect"
import {
  FetchHttpClient,
  HttpClient,
  HttpClientRequest,
  HttpClientResponse
} from "effect/unstable/http"

class Repo extends Schema.Class<Repo>("Repo")({
  id: Schema.Number,
  full_name: Schema.String,
  stargazers_count: Schema.Number
}) {}

export class GitHub extends Context.Service<GitHub, {
  getRepo(owner: string, name: string): Effect.Effect<Repo, GitHubError>
}>()("app/GitHub") {
  static readonly layer = Layer.effect(
    GitHub,
    Effect.gen(function*() {
      const client = (yield* HttpClient.HttpClient).pipe(
        // Base URL + Accept header on every request.
        HttpClient.mapRequest(flow(
          HttpClientRequest.prependUrl("https://api.github.com"),
          HttpClientRequest.acceptJson
        )),
        // Treat any non-2xx response as a failure.
        HttpClient.filterStatusOk,
        // Give each request 10 seconds before it is interrupted and fails.
        HttpClient.transform(Effect.timeout("10 seconds")),
        // Retry transient failures (network errors, timeouts, 5xx, 429) with an
        // exponential backoff, up to 3 times.
        HttpClient.retryTransient({
          schedule: Schedule.exponential(200),
          times: 3
        })
      )

      const getRepo = Effect.fn("GitHub.getRepo")(
        function*(owner: string, name: string) {
          const response = yield* client.get(`/repos/${owner}/${name}`)
          return yield* HttpClientResponse.schemaBodyJson(Repo)(response)
        },
        // Cross-cutting: wrap any failure as a domain error.
        Effect.mapError((cause) => new GitHubError({ cause }))
      )

      return GitHub.of({ getRepo })
    })
  ).pipe(Layer.provide(FetchHttpClient.layer))
}

export class GitHubError extends Schema.TaggedErrorClass<GitHubError>()(
  "GitHubError",
  { cause: Schema.Defect }
) {}
```

## Failing on bad status

By default the client does **not** fail on a `4xx` or `5xx` — it returns the
response so you can inspect it. To turn a bad status into a typed failure, add
`HttpClient.filterStatusOk` (2xx passes, everything else fails with
`HttpClientError`). For a custom predicate, use `HttpClient.filterStatus`:

```ts
import { HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

// Accept 2xx and the redirect range, fail on everything else.
const client = base.pipe(
  HttpClient.filterStatus((status) => status < 400)
)
```

## Retries

`HttpClient.retryTransient` is the batteries-included retry. It already knows
which failures are transient — connection (`TransportError`) failures,
`TimeoutError`, and retryable status codes (`408`, `429`, `500`, `502`, `503`,
`504`) — so you usually only supply a schedule and a cap:

```ts
import { Schedule } from "effect"
import { HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

const client = base.pipe(
  HttpClient.retryTransient({
    // Backoff between attempts. See the scheduling docs for richer policies.
    schedule: Schedule.exponential("100 millis"),
    // Maximum number of retries.
    times: 5,
    // Choose what counts: "errors-only", "response-only", or (default)
    // "errors-and-responses".
    retryOn: "errors-and-responses"
  })
)
```

When you need full control over *which* errors to retry — including your own
domain errors — use `HttpClient.retry`, which takes an `Effect.retry` options
object or a raw `Schedule`:

```ts
import { Schedule } from "effect"
import { HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

const client = base.pipe(
  HttpClient.retry({
    schedule: Schedule.exponential("100 millis"),
    // Only retry while the predicate holds.
    while: (error) => error.reason._tag === "TransportError"
  })
)
```
**Tip:** Order matters. Put `filterStatusOk`/`filterStatus` **before** the retry so that a
bad status becomes a failure the retry can see. If you retry first and filter
after, the retry never observes the status as an error. See
[Scheduling](https://effect.plants.sh/scheduling/) for how to build and combine retry schedules.

## Timeouts

Apply a timeout per request with `HttpClient.transform`, lifting `Effect.timeout`
over the response effect. A timed-out request is interrupted and fails with a
`TimeoutError`, which `retryTransient` then treats as transient:

```ts
import { Effect } from "effect"
import { HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

const client = base.pipe(
  // Each request must complete within 5 seconds.
  HttpClient.transform(Effect.timeout("5 seconds"))
)
```

## Rate limiting

For client-side rate limiting, `HttpClient.withRateLimiter` integrates with the
`RateLimiter` service from `effect/unstable/persistence/RateLimiter`. It can
share a limit across requests by key, update the limit by reading standard rate
limit response headers, and automatically retry `429` responses through the
limiter:

```ts
import { Duration } from "effect"
import { HttpClient } from "effect/unstable/http"
import { RateLimiter } from "effect/unstable/persistence/RateLimiter"

declare const base: HttpClient.HttpClient
declare const limiter: RateLimiter

const client = base.pipe(
  HttpClient.withRateLimiter({
    limiter,
    window: Duration.seconds(1),
    limit: 10,
    // Requests sharing a key share the limit — e.g. per-host or per-user.
    key: (request) => new URL(request.url).host
  })
)
```

## Configuring the underlying fetch

`FetchHttpClient` lets you override two things from context: the `Fetch`
reference (the `fetch` implementation, useful for testing or a polyfill) and the
`RequestInit` service (default fetch options such as `credentials` or `cache`).
Provide them through context to change transport behavior without touching your
request code:

```ts
import { Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"

// Send credentials with every request.
const FetchLayer = FetchHttpClient.layer.pipe(
  Layer.provide(
    Layer.succeed(FetchHttpClient.RequestInit)({ credentials: "include" })
  )
)
```

---

## API reference

Every combinator below returns a **new** `HttpClient` with behavior layered
around the previous one, so they all compose with `.pipe`. Combinators that add
new error or requirement types widen the client's `E`/`R` accordingly
(`HttpClient.HttpClient` is `HttpClient.With<HttpClientError>`).

### `HttpClient.HttpClient`

The service tag for the default outgoing client. Access it inside a `Layer` or
`Effect.gen`, configure it, then store the configured client in your own
service.

```ts
import { Effect } from "effect"
import { HttpClient } from "effect/unstable/http"

const program = Effect.gen(function*() {
  const client = yield* HttpClient.HttpClient
  const response = yield* client.get("https://api.example.com/health")
  return response.status
  // => 200
})
```

The module also re-exports accessor versions of every method
(`HttpClient.get`, `head`, `post`, `patch`, `put`, `del`, `options`, and
`HttpClient.execute` for a prebuilt request). They read the `HttpClient` service
from context, so you do not have to `yield*` the client first:

```ts
import { HttpClient } from "effect/unstable/http"

// Effect<HttpClientResponse, HttpClientError, HttpClient>
const health = HttpClient.get("https://api.example.com/health")
```

### `make`

Builds an `HttpClient` from a low-level request runner. The runner receives the
request, the resolved `URL`, an `AbortSignal`, and the current fiber. The
wrapper handles URL construction failures, tracing, header redaction, and
aborting non-scoped requests on interruption — so a transport only has to
produce a response.

```ts
import { Effect } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"

// A trivial transport backed by globalThis.fetch.
const client = HttpClient.make((request, url, signal) =>
  Effect.map(
    Effect.promise(() => fetch(url, { method: request.method, signal })),
    (web) => HttpClientResponse.fromWeb(request, web)
  )
)
// => HttpClient
```

### `makeWith`

The most general constructor: provide a `postprocess` function (turns the
preprocessed request effect into a response effect) and a `preprocess` function
(transforms the request before execution). Most combinators are implemented in
terms of `makeWith`; reach for it only when adapting an unusual transport.

```ts
import { Effect } from "effect"
import { HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

// Re-wrap an existing client without changing behavior.
const client = HttpClient.makeWith(
  (request) => base.postprocess(request),
  (request) => base.preprocess(request)
)
// => HttpClient
```

### `isHttpClient`

Type guard that returns `true` for any value carrying the `HttpClient` type id.

```ts
import { HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

HttpClient.isHttpClient(base) // => true
HttpClient.isHttpClient({}) // => false
```

### `layerMergedContext`

Wraps an `Effect<HttpClient>` into a `Layer<HttpClient>` whose response effects
merge in the context captured at layer construction time. This is how
`FetchHttpClient.layer` is built — services available where the layer is created
remain available to every request the client makes.

```ts
import { Effect } from "effect"
import { HttpClient } from "effect/unstable/http"

declare const buildClient: Effect.Effect<HttpClient.HttpClient>

const layer = HttpClient.layerMergedContext(buildClient)
// => Layer<HttpClient>
```

## Status filtering

By default a non-2xx response is a *successful* Effect — the response is
returned so you can inspect it. These combinators turn unwanted statuses into a
typed `HttpClientError` (or your own error) so retries and recovery can see
them.

### `filterStatusOk`

Fails with `HttpClientError` unless the response status is in the 2xx range.

```ts
import { HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

const client = HttpClient.filterStatusOk(base)
// 404 => fails with HttpClientError (StatusCodeError)
// 200 => succeeds with the response
```

### `filterStatus`

Fails with `HttpClientError` unless your predicate accepts the numeric status.

```ts
import { HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

// Accept 2xx and 3xx, fail on 4xx/5xx.
const client = base.pipe(
  HttpClient.filterStatus((status) => status < 400)
)
```

### `filterOrElse`

Keeps responses that match the predicate; for the rest it runs an alternative
effect that produces a replacement response (or fails). Useful for fallbacks.

```ts
import { Effect } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"

declare const base: HttpClient.HttpClient
declare const fallback: HttpClientResponse.HttpClientResponse

const client = base.pipe(
  HttpClient.filterOrElse(
    (response) => response.status === 200,
    // Substitute a cached/fallback response for anything else.
    (_response) => Effect.succeed(fallback)
  )
)
```

### `filterOrFail`

Keeps responses that match the predicate; for the rest it fails with an error
you compute from the response. Use it to attach a domain error instead of the
generic `HttpClientError`.

```ts
import { HttpClient } from "effect/unstable/http"

class NotOk {
  readonly _tag = "NotOk"
  constructor(readonly status: number) {}
}

declare const base: HttpClient.HttpClient

const client = base.pipe(
  HttpClient.filterOrFail(
    (response) => response.status < 400,
    (response) => new NotOk(response.status)
  )
)
// 503 => fails with NotOk { status: 503 }
```

## Retries

### `retryTransient`

The batteries-included retry: it already classifies transient failures
(`TransportError`, `TimeoutError`, and retryable statuses `408`/`429`/`500`/
`502`/`503`/`504`). Pass an options object or a bare `Schedule`.

Options:

- `schedule` — the backoff `Schedule` between attempts.
- `times` — maximum number of retries.
- `retryOn` — `"errors-only"` (retry failed effects), `"response-only"` (retry
  successful-but-transient responses like a `503` you have *not* filtered), or
  the default `"errors-and-responses"` (both).
- `while` — an extra predicate that marks additional errors as transient
  (ignored in `"response-only"` mode).

```ts
import { Schedule } from "effect"
import { HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

const client = base.pipe(
  HttpClient.retryTransient({
    schedule: Schedule.exponential("100 millis"),
    times: 5,
    retryOn: "errors-and-responses",
    // Also retry our own InvalidUrlError-style transports if they showed up.
    while: (error) => error.reason._tag === "TransportError"
  })
)

// Bare schedule form (equivalent to retryOn: "errors-and-responses"):
const simple = base.pipe(HttpClient.retryTransient(Schedule.recurs(3)))
```

### `retry`

The general retry. Pass an `Effect.retry` options object (`schedule`, `times`,
`while`, `until`, …) or a raw `Schedule` — nothing is classified as transient
for you, so the predicate decides exactly what to retry.

```ts
import { Schedule } from "effect"
import { HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

const client = base.pipe(
  HttpClient.retry({
    schedule: Schedule.exponential("100 millis"),
    times: 3,
    while: (error) => error.reason._tag === "StatusCodeError"
  })
)

// Raw schedule form:
const fixed = base.pipe(HttpClient.retry(Schedule.spaced("1 second")))
```

## Request transforms

These rewrite the outgoing request. `mapRequest*` runs **after** existing
request middleware (it appends); `mapRequestInput*` runs **before** it (it
prepends). The effectful variants can fail or read from context.

### `mapRequest`

Appends a pure transformation of the request — base URLs, default headers,
static auth.

```ts
import { flow } from "effect"
import { HttpClient, HttpClientRequest } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

const client = base.pipe(
  HttpClient.mapRequest(flow(
    HttpClientRequest.prependUrl("https://api.example.com"),
    HttpClientRequest.acceptJson
  ))
)
```

### `mapRequestEffect`

Appends an *effectful* transformation — use it when computing the new request is
itself an effect, e.g. fetching a fresh token.

```ts
import { Effect } from "effect"
import { HttpClient, HttpClientRequest } from "effect/unstable/http"

declare const base: HttpClient.HttpClient
declare const loadToken: Effect.Effect<string>

const client = base.pipe(
  HttpClient.mapRequestEffect((request) =>
    Effect.map(loadToken, (token) =>
      HttpClientRequest.bearerToken(request, token)
    )
  )
)
```

### `mapRequestInput`

Prepends a pure transformation — it runs *before* the client's existing request
middleware. Use it to seed defaults that later middleware may override.

```ts
import { HttpClient, HttpClientRequest } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

const client = base.pipe(
  HttpClient.mapRequestInput(
    HttpClientRequest.setHeader("x-request-id", crypto.randomUUID())
  )
)
```

### `mapRequestInputEffect`

Prepends an *effectful* transformation, running before existing middleware.

```ts
import { Effect } from "effect"
import { HttpClient, HttpClientRequest } from "effect/unstable/http"

declare const base: HttpClient.HttpClient
declare const nextRequestId: Effect.Effect<string>

const client = base.pipe(
  HttpClient.mapRequestInputEffect((request) =>
    Effect.map(nextRequestId, (id) =>
      HttpClientRequest.setHeader(request, "x-request-id", id)
    )
  )
)
```

## Response transforms

### `transform`

Wraps the response effect, with access to **both** the response effect and the
original request. This is the most flexible postprocessing hook — timeouts,
per-request fallbacks, request-aware retries.

```ts
import { Effect } from "effect"
import { HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

const client = base.pipe(
  HttpClient.transform((effect, request) =>
    request.method === "GET"
      ? Effect.timeout(effect, "5 seconds") // only time out reads
      : effect
  )
)
```

### `transformResponse`

Wraps the response effect without the request argument. Many combinators
(`filterStatusOk`, `retry`, `catch`, `tap`) are thin wrappers over this.

```ts
import { Effect } from "effect"
import { HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

const client = base.pipe(
  HttpClient.transformResponse(Effect.timeout("5 seconds"))
)
```

### `withScope`

Ties each request's lifetime to the surrounding `Scope` instead of the
individual request. The underlying connection is aborted when the scope closes —
useful when you stream a response body across several steps and want the socket
held open for that whole scope. Adds `Scope` to the client's requirements.

```ts
import { HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

const client = HttpClient.withScope(base)
// => HttpClient.With<HttpClientError, Scope>
```

## Observation taps

Taps run effects for their side effects (logging, metrics) without changing the
request, response, or error that flows through.

### `tapRequest`

Runs an effect on the request *before* it is sent.

```ts
import { Effect } from "effect"
import { HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

const client = base.pipe(
  HttpClient.tapRequest((request) =>
    Effect.log(`${request.method} ${request.url}`)
  )
)
// logs e.g. "GET https://api.example.com/users"
```

### `tap`

Runs an effect on a *successful* response.

```ts
import { Effect } from "effect"
import { HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

const client = base.pipe(
  HttpClient.tap((response) =>
    Effect.log(`status ${response.status}`)
  )
)
```

### `tapError`

Runs an effect on a *failure*, leaving the error in place.

```ts
import { Effect } from "effect"
import { HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

const client = base.pipe(
  HttpClient.tapError((error) =>
    Effect.logError("HTTP request failed", error)
  )
)
```

## Error recovery

### `catch`

Handles **all** client failures with a recovery effect that produces a
replacement response. Mirrors `Effect.catch`, scoped to the client.

```ts
import { Effect } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"

declare const base: HttpClient.HttpClient
declare const cached: HttpClientResponse.HttpClientResponse

const client = base.pipe(
  HttpClient.filterStatusOk,
  HttpClient.catch((_error) => Effect.succeed(cached))
)
```

### `catchTag`

Recovers only failures whose `_tag` matches the given tag (or array of tags),
leaving other errors untouched. Pair it with `filterOrFail` to introduce tagged
domain errors at the client level.

```ts
import { Effect } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"

class RateLimited {
  readonly _tag = "RateLimited"
}

declare const base: HttpClient.HttpClient
declare const empty: HttpClientResponse.HttpClientResponse

const client = base.pipe(
  HttpClient.filterOrFail(
    (response) => response.status !== 429,
    () => new RateLimited()
  ),
  HttpClient.catchTag("RateLimited", () => Effect.succeed(empty))
)
```

### `catchTags`

Handles several tagged failures at once via a case map.

```ts
import { Effect } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"

declare const base: HttpClient.HttpClient
declare const fallback: HttpClientResponse.HttpClientResponse

const client = base.pipe(
  HttpClient.catchTags({
    HttpClientError: (_error) => Effect.succeed(fallback)
  })
)
```

## Redirects & cookies

### `followRedirects`

Transparently follows `3xx` responses that carry a `location` header, re-issuing
the request against the new URL up to `maxRedirects` times (default `10`).

```ts
import { HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

// Follow up to 5 redirects automatically.
const client = HttpClient.followRedirects(base, 5)
```

### `withCookiesRef`

Backs the client with a shared `Ref<Cookies>`: response `Set-Cookie` values are
merged into the ref and sent as a `cookie` header on subsequent requests —
session continuity without a cookie jar at every call site.

```ts
import { Effect, Ref } from "effect"
import { Cookies, HttpClient } from "effect/unstable/http"

declare const base: HttpClient.HttpClient

const program = Effect.gen(function*() {
  const jar = yield* Ref.make(Cookies.empty)
  const client = HttpClient.withCookiesRef(base, jar)
  yield* client.get("https://api.example.com/login") // stores cookies
  return yield* client.get("https://api.example.com/me") // resends them
})
```

## Rate limiting

### `withRateLimiter`

Applies client-side rate limiting using the `RateLimiter` service. It inspects
common rate limit response headers (`ratelimit-limit`, `ratelimit-remaining`,
`retry-after`, …) to update the limit on the fly, and automatically retries
`429` responses — including `HttpClientError`s wrapping a `429` — by routing the
retry back through the limiter. Adds `RateLimiterError` to the client's errors.

`WithRateLimiter.Options`:

- `limiter` — the `RateLimiter` service that gates requests.
- `window` — `Duration.Input` for the initial limit window.
- `limit` — initial maximum number of requests allowed in the window.
- `key` — a fixed string or `(request) => string`; requests sharing a key share
  a limit (per-host, per-user, per-endpoint).
- `algorithm` — `"fixed-window"` (default) or `"token-bucket"`.
- `tokens` — cost of a request, a number or `(request) => number` (default `1`).
- `disableResponseInspection` — when `true`, skip reading limits and
  `retry-after` from response headers.

```ts
import { Duration } from "effect"
import { HttpClient } from "effect/unstable/http"
import { RateLimiter } from "effect/unstable/persistence/RateLimiter"

declare const base: HttpClient.HttpClient
declare const limiter: RateLimiter

const client = base.pipe(
  HttpClient.withRateLimiter({
    limiter,
    window: Duration.seconds(1),
    limit: 10,
    key: (request) => new URL(request.url).host,
    algorithm: "token-bucket",
    tokens: 1,
    disableResponseInspection: false
  })
)
// => HttpClient.With<HttpClientError | RateLimiterError>
```

## Tracing references

Every request is wrapped in a client span automatically, with attributes for
method, URL, headers (redacted), and response status. These
`Context.Reference`s tune that behavior; override them via `Layer` or
`Effect.provideService`.

### `SpanNameGenerator`

Computes the span name for an outgoing request. Defaults to
`` `http.client ${request.method}` ``.

```ts
import { Effect } from "effect"
import { HttpClient } from "effect/unstable/http"

const program = Effect.provideService(
  Effect.void,
  HttpClient.SpanNameGenerator,
  (request) => `fetch ${new URL(request.url).pathname}`
)
// span named e.g. "fetch /users"
```

### `TracerDisabledWhen`

A predicate that disables tracing for matching requests. Defaults to never
disabling.

```ts
import { Effect } from "effect"
import { HttpClient } from "effect/unstable/http"

const program = Effect.provideService(
  Effect.void,
  HttpClient.TracerDisabledWhen,
  (request) => request.url.includes("/healthz") // don't trace health checks
)
```

### `TracerPropagationEnabled`

Controls whether the outgoing span is propagated to request headers (W3C trace
context). Defaults to `true`.

```ts
import { Effect } from "effect"
import { HttpClient } from "effect/unstable/http"

const program = Effect.provideService(
  Effect.void,
  HttpClient.TracerPropagationEnabled,
  false // don't inject traceparent headers (e.g. calling a 3rd-party API)
)
```

## Configuring fetch

### `FetchHttpClient.layer`

Provides an `HttpClient` backed by the Web Fetch API. Use it in browsers, edge
runtimes, and Node where `globalThis.fetch` exists.

```ts
import { Effect } from "effect"
import { FetchHttpClient, HttpClient } from "effect/unstable/http"

const program = Effect.gen(function*() {
  const client = yield* HttpClient.HttpClient
  return yield* client.get("https://api.example.com/ping")
}).pipe(Effect.provide(FetchHttpClient.layer))
```

### `FetchHttpClient.Fetch`

A `Context.Reference` for the `fetch` implementation, defaulting to
`globalThis.fetch`. Override it to inject a polyfill or capture requests in
tests.

```ts
import { Effect, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"

const stubFetch: typeof globalThis.fetch = async () =>
  new Response(JSON.stringify({ ok: true }), { status: 200 })

const TestFetch = FetchHttpClient.layer.pipe(
  Layer.provide(Layer.succeed(FetchHttpClient.Fetch)(stubFetch))
)
```

### `FetchHttpClient.RequestInit`

A `Context.Service` carrying default `RequestInit` options (credentials, cache,
redirect, integrity, …) applied to every request before request-specific fields
override them.

```ts
import { Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"

const FetchLayer = FetchHttpClient.layer.pipe(
  Layer.provide(
    Layer.succeed(FetchHttpClient.RequestInit)({
      credentials: "include",
      cache: "no-store"
    })
  )
)
```

See [Platform](https://effect.plants.sh/platform/) for the platform-specific clients, and
[Observability](https://effect.plants.sh/observability/) — every request is traced automatically, so
spans, timeouts, and retries all show up in your telemetry. See
[Error Management](https://effect.plants.sh/error-management/) for the `Effect`-level combinators these
client-level helpers mirror.