Error Handling¶
Robust error handling is the difference between an application that fails gracefully and one that leaves users staring at a blank screen. This chapter covers error handling on both sides of the stack — mapping Gaia API errors to your backend's HTTP responses, and catching those errors in the React frontend.
Gaia API Error Responses¶
When Gaia returns an error, the response body typically follows this shape:
Or in some cases:
The HTTP status codes you'll encounter most often:
| Status | Meaning | Common Cause |
|---|---|---|
| 401 | Unauthorized | Invalid or expired API key |
| 403 | Forbidden | Key lacks permission for this operation |
| 404 | Not Found | Dataset or resource doesn't exist |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Gaia-side failure |
| 502/503 | Bad Gateway / Unavailable | Gaia cluster is down or unreachable |
Backend Error Handling¶
The SDK Exception Hierarchy¶
The gaia_sdk maps these status codes to typed exceptions:
GaiaError (base)
├── GaiaAuthError (401, 403)
├── GaiaNotFoundError (404)
├── GaiaRateLimitError (429)
├── GaiaServerError (5xx)
└── GaiaTimeoutError (timeout)
Every exception carries status_code and response_body attributes for inspection.
Mapping Gaia Errors to Your HTTP Responses¶
Create a centralized error mapping module that translates SDK exceptions into FastAPI HTTPException responses with a consistent error envelope.
utils/errors.py¶
"""Error mapping utilities."""
from fastapi import HTTPException, status
from gaia_sdk.exceptions import (
GaiaAuthError,
GaiaError,
GaiaNotFoundError,
GaiaRateLimitError,
GaiaServerError,
GaiaTimeoutError,
)
def error_envelope(code: str, message: str) -> dict:
"""Build a standard error response body."""
return {"error": {"code": code, "message": message}}
def raise_http_error(exc: GaiaError) -> None:
"""Map a GaiaError to the appropriate HTTPException and raise it."""
if isinstance(exc, GaiaAuthError):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=error_envelope("unauthorized", str(exc)),
)
if isinstance(exc, GaiaNotFoundError):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=error_envelope("not_found", str(exc)),
)
if isinstance(exc, GaiaRateLimitError):
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail=error_envelope("rate_limited", str(exc)),
headers={"Retry-After": "30"},
)
if isinstance(exc, GaiaTimeoutError):
raise HTTPException(
status_code=status.HTTP_504_GATEWAY_TIMEOUT,
detail=error_envelope("timeout", "The request to Gaia timed out."),
)
if isinstance(exc, GaiaServerError):
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=error_envelope("upstream_error", "Gaia returned a server error."),
)
# Generic GaiaError fallback
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=error_envelope("internal_error", str(exc)),
)
Using raise_http_error in Routes¶
from backend.utils.errors import raise_http_error
from gaia_sdk.exceptions import GaiaError
@router.post("/ask", response_model=AskResponse, tags=["Ask"])
async def ask(
request: AskRequest,
client: GaiaClient = Depends(get_gaia_client),
):
try:
result = await client.ask(
dataset_names=request.dataset_names,
query=request.query,
conversation_id=request.conversation_id,
)
except GaiaError as exc:
raise_http_error(exc)
return AskResponse(
response=result.response_string,
query_uid=result.query_uid,
conversation_id=result.conversation_id,
documents=result.documents,
)
Global Exception Handler¶
For exceptions that slip through route-level handling, register a global exception handler:
# In main.py, inside create_app()
from fastapi import Request
from fastapi.responses import JSONResponse
from gaia_sdk.exceptions import GaiaError
@app.exception_handler(GaiaError)
async def gaia_error_handler(request: Request, exc: GaiaError):
"""Catch any unhandled GaiaError and return a structured response."""
status_map = {
401: 401, 403: 401,
404: 404,
429: 429,
}
http_status = status_map.get(exc.status_code or 0, 502)
return JSONResponse(
status_code=http_status,
content={"error": {"code": "gaia_error", "message": str(exc)}},
)
@app.exception_handler(Exception)
async def generic_error_handler(request: Request, exc: Exception):
"""Catch-all for unexpected errors."""
return JSONResponse(
status_code=500,
content={"error": {"code": "internal_error", "message": "An unexpected error occurred."}},
)
Don't leak internal details
The generic handler should never include stack traces or internal state in the response. Log the full exception server-side and return a safe message to the caller.
Consistent Error Envelope¶
All error responses from your backend should follow the same shape so the frontend can parse them uniformly:
This structure is produced by the error_envelope() helper and used by both raise_http_error() and the global exception handlers.
Frontend Error Handling¶
The ApiError Class¶
The API client (api/client.ts) already throws ApiError for non-2xx responses:
export class ApiError extends Error {
constructor(
public status: number,
public detail: string,
) {
super(detail);
this.name = "ApiError";
}
}
try/catch in API Calls¶
import { ApiError } from "../api/client";
import { ask } from "../api/gaia";
try {
const response = await ask(["my-dataset"], "What is our revenue?");
// handle success
} catch (error) {
if (error instanceof ApiError) {
switch (error.status) {
case 401:
// Session expired — redirect to login
window.location.href = "/login";
break;
case 404:
showToast("Dataset not found.", "error");
break;
case 429:
showToast("Too many requests. Please wait.", "warning");
break;
default:
showToast(error.detail || "Something went wrong.", "error");
}
} else {
showToast("Network error. Check your connection.", "error");
}
}
React Error Boundaries¶
Error boundaries catch rendering errors that would otherwise crash the entire app.
// src/components/ErrorBoundary.tsx
import { Component, type ReactNode } from "react";
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error("ErrorBoundary caught:", error, info);
}
render() {
if (this.state.hasError) {
return (
this.props.fallback ?? (
<div className="p-8 text-center">
<h2 className="text-xl font-bold text-red-600 mb-2">
Something went wrong
</h2>
<p className="text-gray-600">
{this.state.error?.message ?? "An unexpected error occurred."}
</p>
<button
onClick={() => this.setState({ hasError: false, error: null })}
className="mt-4 rounded bg-purple-600 px-4 py-2 text-white"
>
Try Again
</button>
</div>
)
);
}
return this.props.children;
}
}
Wrap your routes with the boundary:
<ErrorBoundary>
<Routes>
<Route path="/chat" element={<ChatPage />} />
</Routes>
</ErrorBoundary>
Toast Notifications¶
For non-fatal errors, use toast notifications. Here's a minimal implementation:
// src/hooks/useToast.ts
import { create } from "zustand";
interface Toast {
id: string;
message: string;
type: "info" | "error" | "warning" | "success";
}
interface ToastState {
toasts: Toast[];
addToast: (message: string, type: Toast["type"]) => void;
removeToast: (id: string) => void;
}
export const useToastStore = create<ToastState>((set) => ({
toasts: [],
addToast: (message, type) => {
const id = `toast-${Date.now()}`;
set((state) => ({
toasts: [...state.toasts, { id, message, type }],
}));
setTimeout(() => {
set((state) => ({
toasts: state.toasts.filter((t) => t.id !== id),
}));
}, 5000);
},
removeToast: (id) =>
set((state) => ({
toasts: state.toasts.filter((t) => t.id !== id),
})),
}));
export function showToast(message: string, type: Toast["type"] = "info") {
useToastStore.getState().addToast(message, type);
}
Retry Strategies for Transient Errors¶
Some errors are transient — the Gaia cluster might be temporarily overloaded, or a network hiccup might cause a timeout. Implement retries with exponential backoff for these cases.
Backend: Retry with httpx¶
"""Retry helper for transient Gaia errors."""
import asyncio
import logging
from gaia_sdk.exceptions import GaiaRateLimitError, GaiaServerError, GaiaTimeoutError
logger = logging.getLogger(__name__)
TRANSIENT_EXCEPTIONS = (GaiaRateLimitError, GaiaServerError, GaiaTimeoutError)
async def with_retry(
func,
*args,
max_retries: int = 3,
base_delay: float = 1.0,
max_delay: float = 30.0,
**kwargs,
):
"""Call an async function with exponential backoff on transient errors."""
last_exception = None
for attempt in range(max_retries + 1):
try:
return await func(*args, **kwargs)
except TRANSIENT_EXCEPTIONS as exc:
last_exception = exc
if attempt == max_retries:
break
delay = min(base_delay * (2 ** attempt), max_delay)
logger.warning(
"Attempt %d/%d failed (%s), retrying in %.1fs",
attempt + 1, max_retries + 1, type(exc).__name__, delay,
)
await asyncio.sleep(delay)
raise last_exception
Usage:
result = await with_retry(
client.ask,
dataset_names=["my-dataset"],
query="What happened yesterday?",
max_retries=2,
)
Frontend: Retry with fetch¶
async function fetchWithRetry(
url: string,
options: RequestInit,
maxRetries = 3,
): Promise<Response> {
let lastError: Error | null = null;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.status === 429 || response.status >= 500) {
throw new Error(`HTTP ${response.status}`);
}
return response;
} catch (error: any) {
lastError = error;
if (attempt < maxRetries) {
const delay = Math.min(1000 * 2 ** attempt, 30000);
await new Promise((r) => setTimeout(r, delay));
}
}
}
throw lastError;
}
Don't retry everything
Only retry on transient errors: timeouts (504), rate limits (429), and server errors (5xx). Never retry on auth errors (401/403) or validation errors (400/422) — those won't succeed on retry.
Summary¶
| Layer | Strategy | Implementation |
|---|---|---|
| SDK | Typed exception hierarchy | GaiaAuthError, GaiaNotFoundError, etc. |
| Backend routes | raise_http_error() pattern | Maps SDK exceptions to HTTPException |
| Backend global | Exception handler middleware | Catches unhandled GaiaError and generic Exception |
| Frontend API | ApiError class | Status-aware error with detail |
| Frontend UI | Error boundaries + toasts | Crash recovery + non-fatal notifications |
| Both | Retry with backoff | Transient errors only |
Next Steps¶
- Streaming Responses — Error handling during SSE streams.
- Session Management — Handling session expiry errors.
- Backend with FastAPI — See error handling in the full backend context.