Error handling
import Callout from "../../../components/docs/Callout.astro"; import ApiTable from "../../../components/docs/ApiTable.astro"; You want to handle errors gracefully — retryable timeouts, fatal auth failures, and everything in between. ### Retryable vs non-retryable errors Every `ArlopassSDKError` has a `retryable` boolean. Timeouts and transient network errors are retryable. Auth failures, policy violations, and state errors are not. Both hooks and the web SDK expose a `retry()` function that replays the last failed operation when the error is retryable. #### React SDK ```tsx title="RetryableErrors.tsx" import { useConnection, useConversation } from "@arlopass/react"; function Chat() { const connection = useConnection(); const { messages, stream, error, retry, isStreaming, streamingContent } = useConversation({ systemPrompt: "You are a helpful assistant.", }); if (connection.error) { return ( <div> <p>Connection error: {connection.error.message}</p> {connection.error.retryable && connection.retry && ( <button onClick={connection.retry}>Retry connection</button> )} </div> ); } if (error) { return ( <div> <p>Chat error: {error.message}</p> <p>Code: {error.machineCode}</p> {error.retryable && retry && ( <button onClick={retry}>Retry last message</button> )} </div> ); } return ( <div> {messages.map((msg) => ( <div key={msg.id}> {msg.role}: {msg.content} </div> ))} {isStreaming && <div>AI: {streamingContent}</div>} </div> ); } ``` #### Web SDK ```ts title="retryable-errors.ts" import { ArlopassClient, ConversationManager } from "@arlopass/web-sdk"; import type { ArlopassSDKError } from "@arlopass/web-sdk"; const client = new ArlopassClient({ transport: window.arlopass }); try { await client.connect({ appId: "my-app" }); } catch (err) { const sdkError = err as ArlopassSDKError; console.error(sdkError.machineCode, sdkError.message); if (sdkError.retryable) { // Safe to retry — transient network issue or timeout await client.connect({ appId: "my-app" }); } else { // Fatal — auth failure, policy violation, etc. throw err; } } const convo = new ConversationManager({ client }); try { for await (const event of convo.stream("Hello!")) { if (event.type === "delta") process.stdout.write(event.content); } } catch (err) { const sdkError = err as ArlopassSDKError; if (sdkError.retryable) { // Retry the stream for await (const event of convo.stream("Hello!")) { if (event.type === "delta") process.stdout.write(event.content); } } } ``` ### ArlopassErrorBoundary Wrap your app (or sections of it) in `ArlopassErrorBoundary` to catch unhandled exceptions. It provides a `fallback` render function and an optional `onError` callback for logging. ```tsx title="ErrorBoundary.tsx" import { ArlopassProvider, ArlopassErrorBoundary } from "@arlopass/react"; function ErrorFallback({ error, resetErrorBoundary, }: { error: Error; resetErrorBoundary: () => void; }) { return ( <div role="alert" style={{ padding: 16, border: "1px solid red" }}> <h3>Something went wrong</h3> <p>{error.message}</p> <button onClick={resetErrorBoundary}>Try again</button> </div> ); } export default function App() { return ( <ArlopassProvider appId="my-app"> <ArlopassErrorBoundary fallback={(props) => <ErrorFallback {...props} />} onError={(error, errorInfo) => { // Log to your error tracking service console.error("Arlopass Error:", error, errorInfo); }} > <Chat /> </ArlopassErrorBoundary> </ArlopassProvider> ); } ``` ### ArlopassHasError guard `ArlopassHasError` is a negative guard — it only renders when there's an active error. Use it in headers, sidebars, or toast areas to show error state outside the main content area. ```tsx title="ErrorBanner.tsx" import { ArlopassHasError } from "@arlopass/react"; function AppHeader() { return ( <header> <h1>My App</h1> <ArlopassHasError> {({ error, retry }) => ( <div style={{ color: "red", padding: 8 }}> Error: {error.message} {error.retryable && retry && ( <button onClick={retry} style={{ marginLeft: 8 }}> Retry </button> )} </div> )} </ArlopassHasError> </header> ); } ``` ### Global error callback Pass `onError` to `ArlopassProvider` to receive every SDK error. Use it for error tracking, analytics, or global logging. ```tsx title="GlobalErrorHandler.tsx" import { ArlopassProvider } from "@arlopass/react"; function App() { return ( <ArlopassProvider appId="my-app" onError={(error) => { // Global error handler — fires for all SDK errors console.error(`[${error.machineCode}] ${error.message}`); // Send to error tracking if (!error.retryable) { trackFatalError(error); } }} > <Chat /> </ArlopassProvider> ); } ``` ### Error codes Key error codes and whether they're retryable: <ApiTable props={[ { name: "ARLOPASS_PROTOCOL_TIMEOUT", type: "retryable", description: "Request timed out — transient network issue", }, { name: "ARLOPASS_PROTOCOL_TRANSIENT_NETWORK", type: "retryable", description: "Temporary network failure", }, { name: "ARLOPASS_PROTOCOL_AUTH_FAILED", type: "fatal", description: "Authentication failed — invalid or expired credentials", }, { name: "ARLOPASS_PROTOCOL_PERMISSION_DENIED", type: "fatal", description: "Insufficient permissions for the requested operation", }, { name: "ARLOPASS_PROTOCOL_POLICY_VIOLATION", type: "fatal", description: "Request violates a configured policy", }, { name: "ARLOPASS_SDK_TRANSPORT_ERROR", type: "varies", description: "Transport-level error — check retryable flag", }, { name: "ARLOPASS_SDK_INVALID_STATE_OPERATION", type: "fatal", description: "Operation attempted in wrong state (e.g. chat before connect)", }, { name: "ARLOPASS_SDK_PROTOCOL_VIOLATION", type: "fatal", description: "Malformed envelope or protocol mismatch", }, ]} /> ### Complete example A chat app with layered error handling — error boundary, connection gate fallbacks, inline chat errors, and global logging: ```tsx title="App.tsx" import { useState } from "react"; import { ArlopassProvider, ArlopassErrorBoundary, ArlopassChatReadyGate, ArlopassHasError, useConversation, } from "@arlopass/react"; function Chat() { const { messages, streamingContent, isStreaming, stream, stop, error, retry, } = useConversation({ systemPrompt: "You are a helpful assistant.", }); const [input, setInput] = useState(""); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!input.trim() || isStreaming) return; const text = input; setInput(""); await stream(text); } return ( <div style={{ maxWidth: 600, margin: "0 auto" }}> <ArlopassHasError> {({ error: connError, retry: connRetry }) => ( <div style={{ padding: 8, background: "#fee", border: "1px solid red" }} > <strong>Error:</strong> {connError.message} {connError.retryable && connRetry && ( <button onClick={connRetry} style={{ marginLeft: 8 }}> Retry </button> )} </div> )} </ArlopassHasError> {error && ( <div style={{ padding: 8, background: "#fff3cd", border: "1px solid orange", }} > <strong>Chat error:</strong> {error.message} {error.retryable && retry && ( <button onClick={retry} style={{ marginLeft: 8 }}> Retry </button> )} </div> )} <div style={{ minHeight: 300, padding: 16 }}> {messages.map((msg) => ( <div key={msg.id} style={{ padding: "8px 0" }}> <strong>{msg.role === "user" ? "You" : "AI"}:</strong> {msg.content} </div> ))} {isStreaming && streamingContent && ( <div style={{ padding: "8px 0", opacity: 0.7 }}> <strong>AI:</strong> {streamingContent} </div> )} </div> <form onSubmit={handleSubmit} style={{ display: "flex", gap: 8 }}> <input value={input} onChange={(e) => setInput(e.target.value)} placeholder="Type a message..." disabled={isStreaming} style={{ flex: 1, padding: 8 }} /> <button type="submit" disabled={isStreaming || !input.trim()}> Send </button> {isStreaming && ( <button type="button" onClick={() => stop()}> Stop </button> )} </form> </div> ); } export default function App() { return ( <ArlopassProvider appId="error-handling-app" onError={(error) => { console.error(`[${error.machineCode}] ${error.message}`); }} > <ArlopassErrorBoundary fallback={({ error, resetErrorBoundary }) => ( <div style={{ padding: 32, textAlign: "center" }}> <h2>Something went wrong</h2> <p>{error.message}</p> <button onClick={resetErrorBoundary}>Try again</button> </div> )} > <ArlopassChatReadyGate connectingFallback={<p>Connecting...</p>} providerFallback={<p>Select a provider in the Arlopass extension.</p>} errorFallback={({ error, retry }) => ( <div> <p>Failed to connect: {error.message}</p> {retry && <button onClick={retry}>Retry</button>} </div> )} > <Chat /> </ArlopassChatReadyGate> </ArlopassErrorBoundary> </ArlopassProvider> ); } ``` <Callout type="tip" title="Related"> See the [Error Codes reference](/docs/reference/web-sdk/error-codes) for the full list, or the [Guard Components guide](/docs/guides/guard-components) for conditional rendering based on error state. </Callout>You want to handle errors gracefully — retryable timeouts, fatal auth failures, and everything in between.
Retryable vs non-retryable errors
Every ArlopassSDKError has a retryable boolean. Timeouts and transient network errors are retryable. Auth failures, policy violations, and state errors are not. Both hooks and the web SDK expose a retry() function that replays the last failed operation when the error is retryable.
React SDK
import { useConnection, useConversation } from "@arlopass/react";
function Chat() {
const connection = useConnection();
const { messages, stream, error, retry, isStreaming, streamingContent } =
useConversation({
systemPrompt: "You are a helpful assistant.",
});
if (connection.error) {
return (
<div>
<p>Connection error: {connection.error.message}</p>
{connection.error.retryable && connection.retry && (
<button onClick={connection.retry}>Retry connection</button>
)}
</div>
);
}
if (error) {
return (
<div>
<p>Chat error: {error.message}</p>
<p>Code: {error.machineCode}</p>
{error.retryable && retry && (
<button onClick={retry}>Retry last message</button>
)}
</div>
);
}
return (
<div>
{messages.map((msg) => (
<div key={msg.id}>
{msg.role}: {msg.content}
</div>
))}
{isStreaming && <div>AI: {streamingContent}</div>}
</div>
);
}
Web SDK
import { ArlopassClient, ConversationManager } from "@arlopass/web-sdk";
import type { ArlopassSDKError } from "@arlopass/web-sdk";
const client = new ArlopassClient({ transport: window.arlopass });
try {
await client.connect({ appId: "my-app" });
} catch (err) {
const sdkError = err as ArlopassSDKError;
console.error(sdkError.machineCode, sdkError.message);
if (sdkError.retryable) {
// Safe to retry — transient network issue or timeout
await client.connect({ appId: "my-app" });
} else {
// Fatal — auth failure, policy violation, etc.
throw err;
}
}
const convo = new ConversationManager({ client });
try {
for await (const event of convo.stream("Hello!")) {
if (event.type === "delta") process.stdout.write(event.content);
}
} catch (err) {
const sdkError = err as ArlopassSDKError;
if (sdkError.retryable) {
// Retry the stream
for await (const event of convo.stream("Hello!")) {
if (event.type === "delta") process.stdout.write(event.content);
}
}
}
ArlopassErrorBoundary
Wrap your app (or sections of it) in ArlopassErrorBoundary to catch unhandled exceptions. It provides a fallback render function and an optional onError callback for logging.
import { ArlopassProvider, ArlopassErrorBoundary } from "@arlopass/react";
function ErrorFallback({
error,
resetErrorBoundary,
}: {
error: Error;
resetErrorBoundary: () => void;
}) {
return (
<div role="alert" style={{ padding: 16, border: "1px solid red" }}>
<h3>Something went wrong</h3>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
export default function App() {
return (
<ArlopassProvider appId="my-app">
<ArlopassErrorBoundary
fallback={(props) => <ErrorFallback {...props} />}
onError={(error, errorInfo) => {
// Log to your error tracking service
console.error("Arlopass Error:", error, errorInfo);
}}
>
<Chat />
</ArlopassErrorBoundary>
</ArlopassProvider>
);
}
ArlopassHasError guard
ArlopassHasError is a negative guard — it only renders when there’s an active error. Use it in headers, sidebars, or toast areas to show error state outside the main content area.
import { ArlopassHasError } from "@arlopass/react";
function AppHeader() {
return (
<header>
<h1>My App</h1>
<ArlopassHasError>
{({ error, retry }) => (
<div style={{ color: "red", padding: 8 }}>
Error: {error.message}
{error.retryable && retry && (
<button onClick={retry} style={{ marginLeft: 8 }}>
Retry
</button>
)}
</div>
)}
</ArlopassHasError>
</header>
);
}
Global error callback
Pass onError to ArlopassProvider to receive every SDK error. Use it for error tracking, analytics, or global logging.
import { ArlopassProvider } from "@arlopass/react";
function App() {
return (
<ArlopassProvider
appId="my-app"
onError={(error) => {
// Global error handler — fires for all SDK errors
console.error(`[${error.machineCode}] ${error.message}`);
// Send to error tracking
if (!error.retryable) {
trackFatalError(error);
}
}}
>
<Chat />
</ArlopassProvider>
);
}
Error codes
Key error codes and whether they’re retryable:
| Prop | Type | Default | Description |
|---|---|---|---|
ARLOPASS_PROTOCOL_TIMEOUT | retryable | — | Request timed out — transient network issue |
ARLOPASS_PROTOCOL_TRANSIENT_NETWORK | retryable | — | Temporary network failure |
ARLOPASS_PROTOCOL_AUTH_FAILED | fatal | — | Authentication failed — invalid or expired credentials |
ARLOPASS_PROTOCOL_PERMISSION_DENIED | fatal | — | Insufficient permissions for the requested operation |
ARLOPASS_PROTOCOL_POLICY_VIOLATION | fatal | — | Request violates a configured policy |
ARLOPASS_SDK_TRANSPORT_ERROR | varies | — | Transport-level error — check retryable flag |
ARLOPASS_SDK_INVALID_STATE_OPERATION | fatal | — | Operation attempted in wrong state (e.g. chat before connect) |
ARLOPASS_SDK_PROTOCOL_VIOLATION | fatal | — | Malformed envelope or protocol mismatch |
Complete example
A chat app with layered error handling — error boundary, connection gate fallbacks, inline chat errors, and global logging:
import { useState } from "react";
import {
ArlopassProvider,
ArlopassErrorBoundary,
ArlopassChatReadyGate,
ArlopassHasError,
useConversation,
} from "@arlopass/react";
function Chat() {
const {
messages,
streamingContent,
isStreaming,
stream,
stop,
error,
retry,
} = useConversation({
systemPrompt: "You are a helpful assistant.",
});
const [input, setInput] = useState("");
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!input.trim() || isStreaming) return;
const text = input;
setInput("");
await stream(text);
}
return (
<div style={{ maxWidth: 600, margin: "0 auto" }}>
<ArlopassHasError>
{({ error: connError, retry: connRetry }) => (
<div
style={{ padding: 8, background: "#fee", border: "1px solid red" }}
>
<strong>Error:</strong> {connError.message}
{connError.retryable && connRetry && (
<button onClick={connRetry} style={{ marginLeft: 8 }}>
Retry
</button>
)}
</div>
)}
</ArlopassHasError>
{error && (
<div
style={{
padding: 8,
background: "#fff3cd",
border: "1px solid orange",
}}
>
<strong>Chat error:</strong> {error.message}
{error.retryable && retry && (
<button onClick={retry} style={{ marginLeft: 8 }}>
Retry
</button>
)}
</div>
)}
<div style={{ minHeight: 300, padding: 16 }}>
{messages.map((msg) => (
<div key={msg.id} style={{ padding: "8px 0" }}>
<strong>{msg.role === "user" ? "You" : "AI"}:</strong> {msg.content}
</div>
))}
{isStreaming && streamingContent && (
<div style={{ padding: "8px 0", opacity: 0.7 }}>
<strong>AI:</strong> {streamingContent}
</div>
)}
</div>
<form onSubmit={handleSubmit} style={{ display: "flex", gap: 8 }}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isStreaming}
style={{ flex: 1, padding: 8 }}
/>
<button type="submit" disabled={isStreaming || !input.trim()}>
Send
</button>
{isStreaming && (
<button type="button" onClick={() => stop()}>
Stop
</button>
)}
</form>
</div>
);
}
export default function App() {
return (
<ArlopassProvider
appId="error-handling-app"
onError={(error) => {
console.error(`[${error.machineCode}] ${error.message}`);
}}
>
<ArlopassErrorBoundary
fallback={({ error, resetErrorBoundary }) => (
<div style={{ padding: 32, textAlign: "center" }}>
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)}
>
<ArlopassChatReadyGate
connectingFallback={<p>Connecting...</p>}
providerFallback={<p>Select a provider in the Arlopass extension.</p>}
errorFallback={({ error, retry }) => (
<div>
<p>Failed to connect: {error.message}</p>
{retry && <button onClick={retry}>Retry</button>}
</div>
)}
>
<Chat />
</ArlopassChatReadyGate>
</ArlopassErrorBoundary>
</ArlopassProvider>
);
}