Skip to content

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 → Retry with exponential backoff
  • 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, unknown>,
  ) {
    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, unknown>,
  ) {
    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, unknown>;
  stack?: string;
  userId?: string;
  sessionId?: string;
}

class ErrorLogger {
  logError(error: Error, context?: Record<string, unknown>): 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

Single-Path Error Propagation

typescript
class AgentStrictErrorHandling {
  async run(input: string): Promise<string> {
    try {
      return await this.primaryProvider.chat(input);
    } catch (error) {
      logger.error('Provider failed', { error });
      throw error;
    }
  }
}

State Recovery

typescript
interface RecoveryCheckpoint {
  conversationHistory: Message[];
  providerState: Record<string, unknown>;
  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, unknown>): string {
    return template.replace(/\{(\w+)\}/g, (match, key) => {
      return context[key]?.toString() || match;
    });
  }
}

Released under the MIT License.