Skip to content
result-rpc
Esc
navigateopen⌘Jpreview
On this page

The client

Every call resolves Result with the complete union; batching, cancellation, and wire-parity server calls.

import { createClient, batchFetchTransport } from "result-rpc/client"
import { appContract } from "../shared/contract"

export const client = createClient({
  contract: appContract,
  transport: batchFetchTransport({ url: "/rpc" }),
})

const result = await client.doc.byId({ id: "doc_123" })

Calls issued in the same microtask share one HTTP request. Every batch item keeps its own status, decoder, rich-value envelope, and tagged Result. Use fetchTransport when batching is not wanted; the client API is unchanged.

The direct client is the honest base: it always resolves the complete union. Narrowing is a property of where a call is rendered, and the direct client is not rendered anywhere, so it never subtracts anything.

Result<
  Doc,
  | Unauthorized
  | DocNotFound
  | ServerInternal
  | Offline
  | NetworkFailure
  | Timeout
  | HttpFailure
  | ProtocolViolation
  | DecodeFailure
>

Handle the union with an ordinary switch (result.error satisfies never in the default arm keeps it exhaustive), or build a reusable projection — a message catalog, a metrics mapper — once, from the same definition map middleware and shells use:

import { errorCatalog } from "result-rpc"

const message = errorCatalog({ DocNotFound, Unauthorized }, {
  "doc/not-found": (e) => `Doc ${e.data.docId} is gone`,
  "auth/unauthorized": () => "Sign in to continue",
})

Adding a definition to the map breaks every catalog missing the new tag. For inline one-offs, matchError(result.error, { ...handlers }) gives the same exhaustiveness on a single value.

Cancellation is not an operation error

Query cancellation updates lifecycle state without producing an Err, consuming a retry, or entering an error boundary.

Direct calls accept an AbortSignal:

const controller = new AbortController()

const pending = client.doc.byId(
  { id: "doc_123" },
  { signal: controller.signal },
)

controller.abort()

Internally result-rpc uses a tagged control/cancelled sentinel so cancellation does not depend on platform-specific AbortError identity. It is control flow and is excluded from the recoverable error union. Its sibling, control/claimed, is what a shell-claimed mutation rejects with — the same family, one catch at the call site, and isCancelled/isClaimed to tell the two events apart when the UX differs.

Server-side calls keep wire parity

import { createServerClient } from "result-rpc/server"
import { appRouter } from "./router"

const serverClient = createServerClient(appRouter, {
  mode: "parity",
  context,
})

const result = await serverClient.doc.byId({ id: "doc_123" })

Parity mode executes locally but still applies input, output, and error codecs. A value that would fail remotely also fails during SSR, tests, and server components. Parity is the only mode for now; an unchecked fast path can follow if profiling ever demands one.

Was this page helpful?