Skip to content
result-rpc
Esc
navigateopen⌘Jpreview
The migration path for the tRPC app that grew up

One Result.
One error union.
Server to screen.

Every way an operation can fail is a typed, wire-safe value in its own closed union — and responsibility for each failure is assigned to exactly one place in the component tree.

// the honest base: the complete union
const query = useResultQuery(client.doc.byId, { id })
// DocNotFound | Unauthorized | ServerInternal | Offline |
// NetworkFailure | Timeout | HttpFailure |
// ProtocolViolation | DecodeFailure | Stale

// under the onion: each shell subtracted what it owns
const query = AuthShell.useQuery(client.doc.byId, { id })
// DocNotFound — and a missing case fails to compile
Nothing was hidden. Each tag left the union because a named owner took responsibility for it.

Every tag has exactly one owner.

01

Components

Branch on domain errors only — exhaustively, or it fails to compile.

02

Shells

Claim transport blips, auth expiry, and defects at the tree position that owns them.

03

Entities

Mutations return who changed; every cached view patches in place. Zero refetches.

04

Deploys

Contract digests detect stale clients; the reload is the fix, not a mystery incident.

tRPC ergonomics. Effect-grade errors.
A cache that understands identity.

01

Closed unions

Errors are values with owners

Every failure a call can produce is a typed, wire-safe tag in that operation’s own closed union — domain, transport, protocol, and staleness in one place.

02

Error boundaries for values

Shells subtract what they own

Providers claim failure classes ambiently — pause and resume, or escalate to the real error boundary — and remove those tags from the unions components see.

03

The identity graph

Update by model + id

defineModel gives values identity. Change an avatar and the header, bylines, and member rows patch in place — automatic invalidation and updates, no normalized store.

Migrate per router.
Start with the auth layer.