Overview
Proper error handling ensures your AI-powered application gracefully handles failures from network issues, API errors, tool failures, and validation problems. Tambo provides structured error types and patterns for handling errors at different layers of your application.Error Types
Tambo exports error types from the TypeScript SDK:import type { TamboAIError, APIError, RateLimitError } from '@tambo-ai/react';
TamboAIError
Base error class for all Tambo errors:class TamboAIError extends Error {
message: string; // Error description
name: string; // Error type name
}
APIError
Errors from API requests:class APIError extends TamboAIError {
status: number; // HTTP status code
headers: Headers; // Response headers
}
RateLimitError
Rate limit exceeded errors:class RateLimitError extends APIError {
status: 429;
}
Thread Input Errors
Handle errors when submitting messages:components/chat.tsx
import { useTamboThreadInput, useTambo } from '@tambo-ai/react';
import type { TamboAIError } from '@tambo-ai/react';
function ChatInput() {
const { value, setValue, submit, isPending } = useTamboThreadInput();
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
try {
await submit();
} catch (err) {
if (err instanceof TamboAIError) {
setError(err.message);
} else {
setError('An unexpected error occurred. Please try again.');
}
}
};
return (
<form onSubmit={handleSubmit}>
{error && (
<div className="bg-red-50 border border-red-200 rounded p-3 mb-2">
<p className="text-red-800 text-sm">{error}</p>
</div>
)}
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
disabled={isPending}
/>
<button type="submit" disabled={isPending || !value.trim()}>
Send
</button>
</form>
);
}
API Errors
Handle different HTTP error statuses:import type { APIError } from '@tambo-ai/react';
try {
await submit();
} catch (err) {
if (err instanceof APIError) {
switch (err.status) {
case 400:
setError('Invalid request. Please check your input.');
break;
case 401:
setError('Authentication failed. Please sign in again.');
// Redirect to login
break;
case 403:
setError('You do not have permission to perform this action.');
break;
case 404:
setError('Resource not found.');
break;
case 429:
setError('Rate limit exceeded. Please wait a moment and try again.');
break;
case 500:
case 502:
case 503:
setError('Server error. Please try again later.');
break;
default:
setError(`Error: ${err.message}`);
}
}
}
Rate Limit Errors
Handle rate limiting with retry logic:import type { RateLimitError } from '@tambo-ai/react';
function ChatInput() {
const { submit } = useTamboThreadInput();
const [retryAfter, setRetryAfter] = useState<number | null>(null);
const handleSubmit = async () => {
try {
await submit();
} catch (err) {
if (err instanceof RateLimitError) {
// Get retry-after header (seconds)
const retryHeader = err.headers.get('retry-after');
const seconds = retryHeader ? parseInt(retryHeader, 10) : 60;
setRetryAfter(seconds);
// Auto-retry after delay
setTimeout(() => {
setRetryAfter(null);
handleSubmit(); // Retry
}, seconds * 1000);
}
}
};
if (retryAfter) {
return (
<div className="text-amber-600">
Rate limit exceeded. Retrying in {retryAfter} seconds...
</div>
);
}
// ... rest of component
}
Tool Errors
Handle errors from local tool execution:lib/tambo-tools.ts
import { TamboTool } from '@tambo-ai/react';
import { z } from 'zod';
const searchTool: TamboTool = {
name: 'search',
description: 'Search the database',
tool: async (params: { query: string }) => {
try {
const response = await fetch(`/api/search?q=${params.query}`);
if (!response.ok) {
// Throw descriptive error for the AI
if (response.status === 404) {
throw new Error('Search endpoint not found');
}
if (response.status === 403) {
throw new Error('You do not have permission to search');
}
throw new Error(`Search failed: ${response.statusText}`);
}
const results = await response.json();
return results;
} catch (err) {
// Catch network errors
if (err instanceof TypeError) {
throw new Error('Network error. Please check your connection.');
}
// Re-throw other errors
throw err;
}
},
inputSchema: z.object({
query: z.string(),
}),
outputSchema: z.any(),
};
Voice Input Errors
Handle microphone and transcription errors:components/voice-button.tsx
import { useTamboVoice } from '@tambo-ai/react';
function VoiceButton() {
const {
startRecording,
stopRecording,
isRecording,
isTranscribing,
transcript,
transcriptionError,
mediaAccessError,
} = useTamboVoice();
const handleStartRecording = () => {
try {
startRecording();
} catch (err) {
console.error('Failed to start recording:', err);
}
};
return (
<div>
{mediaAccessError && (
<div className="bg-red-50 border border-red-200 rounded p-3 mb-2">
<p className="text-red-800 font-medium">Microphone Access Denied</p>
<p className="text-red-600 text-sm">{mediaAccessError}</p>
<p className="text-gray-600 text-sm mt-2">
Please grant microphone access in your browser settings.
</p>
</div>
)}
{transcriptionError && (
<div className="bg-amber-50 border border-amber-200 rounded p-3 mb-2">
<p className="text-amber-800 font-medium">Transcription Failed</p>
<p className="text-amber-600 text-sm">{transcriptionError}</p>
<button
onClick={handleStartRecording}
className="mt-2 text-amber-600 hover:text-amber-800 underline text-sm"
>
Try recording again
</button>
</div>
)}
<button
onClick={isRecording ? stopRecording : handleStartRecording}
disabled={isTranscribing}
>
{isRecording ? 'Stop' : 'Record'}
</button>
</div>
);
}
Component Render Errors
Handle errors in AI-rendered components:components/error-boundary.tsx
import React, { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ComponentErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Component error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
this.props.fallback ?? (
<div className="bg-red-50 border border-red-200 rounded p-4">
<h3 className="text-red-800 font-medium">Component Error</h3>
<p className="text-red-600 text-sm mt-1">
{this.state.error?.message ?? 'An error occurred'}
</p>
</div>
)
);
}
return this.props.children;
}
}
// Wrap AI components
<ComponentErrorBoundary>
<ComponentRenderer content={message.content} />
</ComponentErrorBoundary>
Global Error Handler
Create a centralized error handler:lib/error-handler.ts
import type { TamboAIError, APIError, RateLimitError } from '@tambo-ai/react';
export function handleTamboError(error: unknown): string {
// Unknown error types
if (!(error instanceof Error)) {
console.error('Unknown error:', error);
return 'An unexpected error occurred. Please try again.';
}
// Network errors
if (error instanceof TypeError && error.message.includes('fetch')) {
return 'Network error. Please check your connection.';
}
// Rate limit errors
if (error.name === 'RateLimitError') {
return 'Rate limit exceeded. Please wait a moment and try again.';
}
// API errors
if ('status' in error) {
const apiError = error as APIError;
if (apiError.status >= 500) {
return 'Server error. Please try again later.';
}
if (apiError.status === 401) {
return 'Authentication failed. Please sign in again.';
}
if (apiError.status === 403) {
return 'You do not have permission to perform this action.';
}
}
// Tambo errors
if (error.name === 'TamboAIError') {
return error.message;
}
// Generic error
return error.message || 'An error occurred. Please try again.';
}
// Usage
try {
await submit();
} catch (err) {
const message = handleTamboError(err);
setError(message);
}
Error Recovery
Retry Logic
import { useState } from 'react';
function useRetry<T>(fn: () => Promise<T>, maxRetries = 3) {
const [retryCount, setRetryCount] = useState(0);
const executeWithRetry = async (): Promise<T> => {
try {
const result = await fn();
setRetryCount(0); // Reset on success
return result;
} catch (err) {
if (retryCount < maxRetries) {
setRetryCount(retryCount + 1);
// Exponential backoff
const delay = Math.min(1000 * Math.pow(2, retryCount), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
return executeWithRetry();
}
throw err; // Max retries exceeded
}
};
return { executeWithRetry, retryCount };
}
// Usage
function ChatInput() {
const { submit } = useTamboThreadInput();
const { executeWithRetry, retryCount } = useRetry(submit);
const handleSubmit = async () => {
try {
await executeWithRetry();
} catch (err) {
setError('Failed after 3 attempts. Please try again later.');
}
};
return (
<div>
{retryCount > 0 && (
<div className="text-amber-600 text-sm">
Retrying... (Attempt {retryCount} of 3)
</div>
)}
{/* ... */}
</div>
);
}
Best Practices
Show user-friendly messages
Show user-friendly messages
Translate technical errors into helpful messages:
// Bad
setError(err.message); // "ERR_CONNECTION_REFUSED"
// Good
setError('Unable to connect. Please check your internet connection.');
Log errors for debugging
Log errors for debugging
Always log errors to console in development:
catch (err) {
console.error('Submit failed:', err);
setError(getUserFriendlyMessage(err));
}
Provide recovery actions
Provide recovery actions
Tell users what to do next:
<div className="error-message">
<p>Failed to send message</p>
<button onClick={retry}>Try Again</button>
<button onClick={reportIssue}>Report Issue</button>
</div>
Handle loading states
Handle loading states
Show loading indicators during async operations:
const { isPending } = useTamboThreadInput();
<button disabled={isPending}>
{isPending ? 'Sending...' : 'Send'}
</button>
Test error scenarios
Test error scenarios
Test common failure cases:
- Network offline
- Rate limits
- Invalid API keys
- Malformed responses
Next Steps
Register Tools
Add error handling to local tools
Voice Input
Handle microphone errors
Thread Input Hook
Submit messages with error handling
User Authentication
Handle auth errors