useOnRpcError
One place to react to rpc failures that are nobody's local problem — auth loss, telemetry, toasts.
Some failures do not belong to the component that triggered them. An
Unauthenticated error means the session is gone, whatever screen you happen to
be on. Rather than repeat that check in every useMutation call site, each api's
logical mount carries an error bus; this hook subscribes to it with React
lifecycle. Replacing the socket runtime does not replace that bus, so the
listener stays attached through transport recovery and auth rebuilds.
import { useOnRpcError } from '@voltro/client'
import { errorTag } from '@voltro/protocol'
useOnRpcError('app', useCallback((e) => {
if (e.kind === 'transport') {
showConnectionLost()
return
}
if (errorTag(e.error) === 'Unauthenticated') redirectToSignIn()
}, []))useOnRpcError is the presentation channel. Non-transport failures arrive
immediately. Transport failures wait for the recovery grace window; a gap that
heals in time produces no callback, and an outage that outlives the window
produces exactly one callback even if many subscriptions failed together. Use
useConnectionStatus for the
matching recovering / degraded banner state.
The event is { source, tag, error, kind, outcome?, traceId? }. source is
'mutation' | 'action' | 'subscription', so both unary calls and stream
failures arrive here. error is the value the call failed with — the declared
typed error, an RpcClientError for a transport failure, or the defect — and it
is the same value the failing hook hands over (useSubscription's error, a
rejected mutate), so one _tag switch serves both. errorTag(err) reads
_tag off tagged errors and returns undefined otherwise.
kind is 'transport' | 'handler' | 'client' | 'unknown':
transport— @effect/rpc could not complete the exchange. Use this branch for connection banners and to suppress misleading data-error toasts.handler— the rpc returned a declared failure, such asUnauthenticated.client— framework work around the call failed locally.unknown— a combined or opaque operation could not be classified honestly.
For unary calls, outcome says what can be proved about the server-side result:
not-started, known, or unknown. A transport failure after sending is
unknown even if a trace id exists — the server may have committed before the
response disappeared. Subscription events use not-applicable.
traceId identifies the client call span. When the server accepted the
request, it propagates the same id and voltro logs --trace <id> can join the
call to server logs. A transport error can happen before the server saw the
request, or after it handled the request but before the response returned, so a
traceId on kind: 'transport' is not proof that server logs exist. For the
same reason the event deliberately has no reachedServer boolean.
The callback is held in a ref, so an inline function does not tear down and rebuild the subscription on every render. The bus is per logical api mount — an app talking to several apis subscribes once per api name, not once per connection generation.
You do not need this hook to REPORT rpc failures. Every event this bus emits
is also published on the client error bus, so a reporter — the Sentry browser
integration, or anything wired with subscribeClientErrors — already sees it,
under source: 'rpc.mutation' | 'rpc.action' | 'rpc.subscription' with the rpc
tag, rpcErrorKind, and optional traceId in context. This hook is for
cross-cutting policy (redirect on Unauthenticated, toast on a network
failure), not for telemetry; bridging the two by hand now reports twice.
The reporting bus stays raw: every failure is published immediately, including
a reconnect storm. If you need that same unfiltered stream in React, use
useOnRawRpcError; do not use it for user-facing toasts.
import { useOnRawRpcError } from '@voltro/client'
useOnRawRpcError('app', (event) => diagnostics.record(event))If that is too loud for your reporter, gate it in the subscriber — which failures are worth an event is your policy, and the bus cannot hold one for every subscriber.
This is the read side of failures that already happened on the wire. To push a
client-side error the server never saw, call reportClientError(error, context)
instead. Note also that a subscription failure after data arrived reaches only
this bus, never the hook's error field — see
Queries.