Error Handling Guidelines
This document defines comprehensive error handling strategies for the Robota project.
Error Categories
User Errors
- Configuration Errors: Invalid configuration parameters
- Input Validation Errors: Invalid user input
- Authentication Errors: API key issues, authorization failures
- Usage Errors: Incorrect API usage patterns
System Errors
- Network Errors: API timeouts, connection failures
- Resource Errors: Memory limitations, file system issues
- Provider Errors: AI provider-specific failures
- Internal Errors: Unexpected system states
Recoverable vs Non-Recoverable
Recoverable Errors
- Network timeouts → Retry with backoff
- Rate limiting → Wait and retry
- Temporary provider issues → Fallback to alternative
- Validation errors → Provide correction guidance
Non-Recoverable Errors
- Authentication failures → Require user action
- Malformed configuration → Require configuration fix
- Missing dependencies → Require installation
- Critical system errors → Require restart
Error Design Patterns
Typed Error Classes
typescript
// Base error class
export abstract class RobotaError extends Error {
abstract readonly code: string;
abstract readonly category: 'user' | 'system' | 'provider';
abstract readonly recoverable: boolean;
constructor(
message: string,
public readonly context?: Record<string, any>
) {
super(message);
this.name = this.constructor.name;
}
}
// Specific error implementations
export class ConfigurationError extends RobotaError {
readonly code = 'CONFIGURATION_ERROR';
readonly category = 'user';
readonly recoverable = false;
}
export class ProviderError extends RobotaError {
readonly code = 'PROVIDER_ERROR';
readonly category = 'provider';
readonly recoverable = true;
constructor(
message: string,
public readonly provider: string,
public readonly originalError?: Error,
context?: Record<string, any>
) {
super(message, context);
}
}
Result Pattern for Operations
typescript
// Result type for operations that can fail
type Result<T, E = RobotaError> =
| { success: true; data: T }
| { success: false; error: E };
// Usage example
async function executeOperation(): Promise<Result<string>> {
try {
const result = await riskyOperation();
return { success: true, data: result };
} catch (error) {
return {
success: false,
error: new OperationError('Operation failed', { originalError: error })
};
}
}
Error Handling Strategies
Retry Logic
typescript
interface RetryConfig {
maxAttempts: number;
baseDelay: number;
maxDelay: number;
backoffMultiplier: number;
retryableErrors: string[];
}
class RetryHandler {
async executeWithRetry<T>(
operation: () => Promise<T>,
config: RetryConfig
): Promise<T> {
let lastError: Error;
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (!this.isRetryable(error, config.retryableErrors)) {
throw error;
}
if (attempt === config.maxAttempts) {
break;
}
const delay = Math.min(
config.baseDelay * Math.pow(config.backoffMultiplier, attempt - 1),
config.maxDelay
);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
private isRetryable(error: Error, retryableCodes: string[]): boolean {
if (error instanceof RobotaError) {
return error.recoverable && retryableCodes.includes(error.code);
}
return false;
}
}
Circuit Breaker Pattern
typescript
class CircuitBreaker {
private failures = 0;
private lastFailureTime = 0;
private state: 'closed' | 'open' | 'half-open' = 'closed';
constructor(
private threshold: number,
private timeout: number
) {}
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (Date.now() - this.lastFailureTime < this.timeout) {
throw new CircuitBreakerOpenError('Circuit breaker is open');
}
this.state = 'half-open';
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
this.failures = 0;
this.state = 'closed';
}
private onFailure(): void {
this.failures++;
this.lastFailureTime = Date.now();
if (this.failures >= this.threshold) {
this.state = 'open';
}
}
}
Error Reporting and Logging
Structured Error Logging
typescript
interface ErrorLogEntry {
timestamp: string;
level: 'error' | 'warn';
message: string;
errorCode?: string;
category?: string;
context?: Record<string, any>;
stack?: string;
userId?: string;
sessionId?: string;
}
class ErrorLogger {
logError(error: Error, context?: Record<string, any>): void {
const entry: ErrorLogEntry = {
timestamp: new Date().toISOString(),
level: 'error',
message: error.message,
stack: error.stack,
context
};
if (error instanceof RobotaError) {
entry.errorCode = error.code;
entry.category = error.category;
entry.context = { ...entry.context, ...error.context };
}
// Log to appropriate destination
this.writeLog(entry);
}
}
Error Metrics and Monitoring
typescript
interface ErrorMetrics {
errorCount: Map<string, number>;
errorRate: number;
lastErrorTime: Date;
topErrors: Array<{ code: string; count: number }>;
}
class ErrorMonitor {
private metrics: ErrorMetrics = {
errorCount: new Map(),
errorRate: 0,
lastErrorTime: new Date(),
topErrors: []
};
recordError(error: RobotaError): void {
const count = this.metrics.errorCount.get(error.code) || 0;
this.metrics.errorCount.set(error.code, count + 1);
this.metrics.lastErrorTime = new Date();
this.updateMetrics();
}
getMetrics(): ErrorMetrics {
return { ...this.metrics };
}
}
Error Recovery Strategies
Graceful Degradation
typescript
class AgentWithFallback {
async run(input: string): Promise<string> {
try {
return await this.primaryProvider.chat(input);
} catch (error) {
if (error instanceof ProviderError) {
logger.warn('Primary provider failed, trying fallback', { error });
try {
return await this.fallbackProvider.chat(input);
} catch (fallbackError) {
logger.error('Both providers failed', { error, fallbackError });
return this.generateFallbackResponse(input);
}
}
throw error;
}
}
private generateFallbackResponse(input: string): string {
return "I'm experiencing technical difficulties. Please try again later.";
}
}
State Recovery
typescript
interface RecoveryCheckpoint {
conversationHistory: Message[];
providerState: Record<string, any>;
timestamp: Date;
}
class StateRecoveryManager {
private checkpoints: Map<string, RecoveryCheckpoint> = new Map();
createCheckpoint(sessionId: string, state: RecoveryCheckpoint): void {
this.checkpoints.set(sessionId, state);
}
recoverState(sessionId: string): RecoveryCheckpoint | undefined {
return this.checkpoints.get(sessionId);
}
async handleRecovery(sessionId: string, error: Error): Promise<void> {
const checkpoint = this.recoverState(sessionId);
if (checkpoint) {
logger.info('Recovering from checkpoint', { sessionId, checkpointAge: Date.now() - checkpoint.timestamp.getTime() });
// Restore state logic here
}
}
}
User-Facing Error Messages
Error Message Guidelines
- Be Specific: Clearly explain what went wrong
- Be Actionable: Tell users what they can do to fix it
- Be Helpful: Provide links to documentation or support
- Be Professional: Maintain a helpful, non-technical tone
Message Templates
typescript
const ERROR_MESSAGES = {
INVALID_API_KEY: {
user: "Your API key appears to be invalid. Please check your configuration and ensure you're using a valid API key.",
developer: "Authentication failed with provider {provider}. Verify API key format and permissions.",
action: "Visit {provider} dashboard to generate a new API key"
},
RATE_LIMIT_EXCEEDED: {
user: "You've reached the rate limit for this service. Please wait a moment before trying again.",
developer: "Rate limit exceeded for provider {provider}. Current limit: {limit} requests per {period}.",
action: "Wait {retryAfter} seconds before retrying, or upgrade your plan for higher limits"
},
MODEL_NOT_AVAILABLE: {
user: "The requested AI model is not available. Please try a different model.",
developer: "Model {model} not supported by provider {provider}. Available models: {availableModels}",
action: "Choose from available models: {modelList}"
}
};
Context-Aware Error Formatting
typescript
class ErrorFormatter {
formatError(error: RobotaError, userLevel: 'user' | 'developer' = 'user'): string {
const template = ERROR_MESSAGES[error.code];
if (!template) {
return error.message;
}
const message = template[userLevel] || template.user;
return this.interpolateTemplate(message, error.context || {});
}
private interpolateTemplate(template: string, context: Record<string, any>): string {
return template.replace(/\{(\w+)\}/g, (match, key) => {
return context[key]?.toString() || match;
});
}
}