Error Recovery Patterns
How to handle gecx-chat errors in production React applications. This is a companion to:
- Error Handling — the
ChatSdkErrorstructure and basic try/catch patterns - Error Codes Reference — every code with severity and retryability
This guide focuses on patterns: real code you can copy into your app for each error category.
Quick reference
| Error Category | Example Codes | Recovery Pattern | Section |
|---|---|---|---|
| Authentication | AUTH_EXPIRED, AUTH_FAILED, TOKEN_ENDPOINT_FAILED | Token refresh + retry | §1 |
| Transport & streaming | TRANSPORT_CONNECT_FAILED, STREAM_INTERRUPTED | Built-in automatic recovery | §2 |
| Network | NETWORK_OFFLINE, RECONNECT_EXHAUSTED | Offline UX + manual retry | §3 |
| Tools | TOOL_EXECUTION_FAILED, TOOL_TIMEOUT | Tool-specific retry UX | §4 |
| Sessions | SESSION_EXPIRED, SESSION_ENDED | New session + draft preservation | §5 |
| Uploads | FILE_VALIDATION_FAILED, UPLOAD_FAILED | Inline error + retry button | §6 |
1. Authentication errors
The SDK refreshes tokens automatically when an AuthProvider is configured with a token endpoint. You only need to write recovery code for two cases: when the refresh itself fails, and when credentials are rejected outright.
import { ChatSdkError } from 'gecx-chat';
import { useChatSession } from 'gecx-chat/react';
function ChatComponent() {
const { send, error, status } = useChatSession({
config: { auth: tokenEndpointAuth({ url: '/api/chat-token' }) },
onError: (err) => {
if (!(err instanceof ChatSdkError)) return;
switch (err.code) {
case 'AUTH_EXPIRED':
// SDK retries automatically. Nothing to do.
break;
case 'AUTH_FAILED':
case 'TOKEN_ENDPOINT_INVALID_RESPONSE':
// Hard failure — user needs to re-authenticate. Redirect to login,
// preserving the current path so we can return after sign-in.
window.location.href = `/login?next=${encodeURIComponent(window.location.pathname)}`;
break;
case 'TOKEN_ENDPOINT_FAILED':
// Network error reaching the token endpoint. The SDK already retries
// with backoff. Surface a transient banner only if it persists.
break;
}
},
});
return <ChatSurface status={status} error={error} send={send} />;
}
What the SDK does for you:
- Auto-refreshes tokens before they expire (per
prefetchBufferMs) - Retries token endpoint failures with exponential backoff
- Marks
AUTH_EXPIREDasretryable: trueso retry helpers can act on it
What you must do:
- Handle the unrecoverable case (
AUTH_FAILED) by routing to your sign-in flow - Decide your UX for repeated
TOKEN_ENDPOINT_FAILED(banner, toast, or silent)
2. Transport & streaming recovery
The SDK handles transport errors automatically via the configurable RecoveryPolicy. You rarely need to write recovery code — instead, configure the policy to match your reliability needs.
const client = createChatClient({
transport: createProxyTransport({ endpoint: '/chat/stream' }),
recovery: {
maxAttempts: 5, // total reconnect attempts before giving up
initialBackoffMs: 500, // first retry after 500ms
maxBackoffMs: 30_000, // cap each backoff at 30s
backoffMultiplier: 1.5, // grow backoff by 1.5x each attempt
resumeMode: 'resume', // 'resume' if transport supports it, else 'replay'
},
});
Status transitions the user sees during recovery:
ready → streaming → disconnected → recovering → ready
Render an inline indicator when status === 'recovering':
function StatusBanner({ status }: { status: SessionStatus }) {
if (status === 'recovering') {
return <div role="status">Reconnecting…</div>;
}
if (status === 'disconnected') {
return <div role="alert">Connection lost. Retrying…</div>;
}
return null;
}
What the SDK does for you:
- Reconnects with exponential backoff
- Resumes streams from the last cursor when transport supports it (
resumeMode: 'resume') - Replays the in-flight turn when resume is unavailable (
resumeMode: 'replay') - Deduplicates with idempotency keys so replay is safe
- Emits
RECONNECT_EXHAUSTEDonly aftermaxAttemptsfailures
What you must do:
- Show a recovery banner so the user understands the delay
- Handle
RECONNECT_EXHAUSTED(see §3 — it falls into the network category)
3. Network errors
When the device is offline or recovery exhausts, the SDK exposes status changes you can react to.
function OfflineBanner() {
const { status, error, session } = useChatSession({ /* ... */ });
if (status === 'recovering' || status === 'disconnected') {
return (
<div role="alert" className="banner">
<span>You're offline. Messages will send when connection returns.</span>
<button onClick={() => session?.reconnect()}>Retry now</button>
</div>
);
}
if (error instanceof ChatSdkError && error.code === 'RECONNECT_EXHAUSTED') {
return (
<div role="alert" className="banner banner--error">
<span>Couldn't reconnect. Check your connection and try again.</span>
<button onClick={() => window.location.reload()}>Reload</button>
</div>
);
}
return null;
}
What the SDK does for you:
- Detects
navigator.onLineand queues sends in the offline outbox (whennetwork: 'auto') - Drains the outbox automatically when connectivity returns
- Emits
OUTBOX_DROPPEDif a queued message is too old to send
What you must do:
- Show an offline banner so the user knows their messages are queued, not lost
- Provide a manual "Retry now" affordance for impatient users
- Handle
OUTBOX_DROPPEDif you want to surface dropped queued messages
4. Tool errors
Tool errors come in two flavors: pre-execution (validation, approval denied) and execution-time (timeout, runtime failure). The recovery UX is different for each.
function ToolApprovalDialog() {
const { sendToolResponse, messages } = useChatSession({ /* ... */ });
// Pre-execution: validation or approval denied. The tool never ran.
// Surface a retry that lets the user adjust their input.
function handleValidationFailed(toolCallId: string, code: string) {
if (code === 'TOOL_VALIDATION_FAILED') {
// Show the schema error to the user; let them edit and resubmit.
// sendToolResponse with output: { ok: false, retry: true }
}
}
// Execution-time: tool ran but timed out or threw.
// Most tools should be safe to retry; check sideEffectLevel before assuming.
function handleExecutionFailed(toolCallId: string, code: string) {
if (code === 'TOOL_TIMEOUT') {
return (
<div>
The tool took too long. <button onClick={() => retry(toolCallId)}>Retry</button>
</div>
);
}
if (code === 'TOOL_EXECUTION_FAILED') {
// Don't auto-retry — the failure may be deterministic. Show the user.
return <div>The tool failed. Try a different approach.</div>;
}
}
}
For server tools with side effects: Idempotency keys make retries safe. Verify the tool's idempotency.mode is 'strict' or 'replay' before retrying state-changing tools.
defineServerTool({
name: 'place_order',
// ...
idempotency: {
mode: 'strict', // duplicate idempotencyKey returns the original result
ttlMs: 24 * 60 * 60_000, // remember keys for 24 hours
duplicateBehavior: 'replayed',
},
sideEffectLevel: 'state_changing',
});
What the SDK does for you:
- Validates tool input against the JSON schema before execution
- Enforces the
timeoutMsper tool - Routes errors through the tool registry's lifecycle events
What you must do:
- Decide retry semantics per tool based on
sideEffectLevel - Show validation errors to the user so they can fix their input
- For state-changing tools, ensure idempotency is configured before enabling retry
5. Session expiry
Sessions can end for two reasons: TTL expiry (SESSION_EXPIRED) or explicit termination (SESSION_ENDED). Both leave the user mid-conversation, so preserve their draft input before starting a new session.
function ChatWithRecovery() {
const { send, input, status, reset } = useChatSession({ /* ... */ });
const [pendingDraft, setPendingDraft] = useState<string | null>(null);
// When a session ends, preserve the user's draft for the new session.
useEffect(() => {
if (status === 'expired') {
const draft = input.value;
input.clear();
setPendingDraft(draft);
}
}, [status, input]);
// After reset completes and we're back to ready, restore the draft.
useEffect(() => {
if (status === 'ready' && pendingDraft !== null) {
input.set(pendingDraft);
setPendingDraft(null);
}
}, [status, pendingDraft, input]);
if (status === 'expired') {
return (
<div role="alert">
<p>Your conversation timed out. Start a new one?</p>
<button onClick={() => reset()}>New conversation</button>
</div>
);
}
return <ChatSurface input={input} send={send} />;
}
What the SDK does for you:
- Transitions to status
'expired'when the session TTL elapses - Preserves session-scoped storage until governance retention rules clean it
- Emits
SESSION_EXPIREDas a terminal error (not retryable)
What you must do:
- Detect the expired state and offer a "New conversation" affordance
- Preserve the user's in-progress input across the reset boundary
- If you're tracking conversation history (see Identity and Continuity), the expired session remains in the registry for resumption
6. Upload errors
Upload errors are inherently per-attachment. Show the error inline next to the offending file and offer a retry that exercises retryAttachment.
function AttachmentList() {
const { messages, retryAttachment, removeAttachment } = useChatSession({ /* ... */ });
return (
<ul>
{extractAttachments(messages).map((attachment) => {
if (attachment.status === 'failed') {
return (
<li key={attachment.id}>
<span>{attachment.name}</span>
<ErrorMessage code={attachment.error?.code} />
<button onClick={() => retryAttachment(attachment.id)}>Retry</button>
<button onClick={() => removeAttachment(attachment.id)}>Remove</button>
</li>
);
}
return <li key={attachment.id}>{attachment.name} — {attachment.status}</li>;
})}
</ul>
);
}
function ErrorMessage({ code }: { code?: string }) {
switch (code) {
case 'FILE_VALIDATION_FAILED':
return <span>This file type isn't supported. Try PDF, PNG, or JPG.</span>;
case 'UPLOAD_FAILED':
return <span>Upload didn't complete — check your connection.</span>;
default:
return null;
}
}
What the SDK does for you:
- Validates files against
acceptedTypesandmaxSizeBytesbefore upload - Tracks per-attachment lifecycle (
pending→uploading→complete/failed) - Exposes
retryAttachment(id)to redo a failed upload without re-selecting the file
What you must do:
- Show the failed status inline next to the attachment
- Offer Retry and Remove affordances
- Validate user expectations against your
acceptedTypespolicy in the file picker UX
See also
- Error Codes Reference — full code catalog with
docsUrlanchors - Error Handling —
ChatSdkErrorstructure and basic try/catch - Transport Recovery Contract — the recovery policy spec
- Data Governance — what happens to data after errors and resets
docs/guides/error-recovery.md