Skip to content

Error Handling

Meloqui provides typed errors that help you handle failures gracefully and build resilient applications.

Error Hierarchy

All Meloqui errors extend ChatError, which extends the standard Error:

Error
└── ChatError
    ├── RateLimitError      (retryable)
    ├── TimeoutError        (retryable)
    ├── ServerError         (retryable)
    ├── RagRetrievalError   (retryable)
    ├── AuthenticationError
    ├── InvalidRequestError
    ├── ModelNotFoundError
    ├── CapabilityError
    ├── PluginError
    ├── StorageError
    └── InvalidConversationIdError

Quick Start

typescript
import { ChatClient, ChatError, RateLimitError } from 'meloqui';

try {
  const response = await client.chat('Hello');
} catch (error) {
  if (error instanceof RateLimitError) {
    console.log('Rate limited, wait before retrying');
  } else if (error instanceof ChatError) {
    console.log(`Chat error: ${error.message}`);
  } else {
    throw error; // Re-throw unknown errors
  }
}

Retryable vs Non-Retryable

Every ChatError has an isRetryable property:

typescript
if (error instanceof ChatError && error.isRetryable) {
  // Safe to retry this request
}

Retryable Errors

These errors may succeed if you retry:

ErrorCauseAction
RateLimitErrorToo many requests (429)Wait and retry
TimeoutErrorRequest took too longRetry or increase timeout
ServerErrorProvider server error (5xx)Wait and retry
RagRetrievalErrorEmbedding service failedRetry or continue without context

Non-Retryable Errors

These errors require fixing the request or configuration:

ErrorCauseAction
AuthenticationErrorInvalid API key (401/403)Check credentials
InvalidRequestErrorBad parameters (400)Fix request
ModelNotFoundErrorModel doesn't existCorrect model name
CapabilityErrorFeature not supportedUse different approach
PluginErrorPlugin misconfiguredFix plugin config
StorageErrorStorage operation failedCheck storage
InvalidConversationIdErrorInvalid conversation IDFix ID format

Error Properties

ChatError (Base)

typescript
interface ChatError extends Error {
  readonly isRetryable: boolean;   // Can this be retried?
  readonly provider?: string;      // Which provider threw it?
  readonly originalError?: Error;  // Underlying SDK error
}

CapabilityError

typescript
interface CapabilityError extends ChatError {
  readonly capability: string;  // e.g., "vision", "streaming"
}

StorageError

typescript
interface StorageError extends ChatError {
  readonly operation: string;       // "save", "load", "delete", "list"
  readonly conversationId?: string; // Affected conversation
}

Handling Patterns

Basic Try-Catch

typescript
import { ChatError } from 'meloqui';

try {
  await client.chat('Hello');
} catch (error) {
  if (error instanceof ChatError) {
    console.error(`Provider: ${error.provider}`);
    console.error(`Retryable: ${error.isRetryable}`);
    console.error(`Message: ${error.message}`);
  }
}

Specific Error Handling

Handle different errors differently:

typescript
import {
  RateLimitError,
  AuthenticationError,
  TimeoutError,
  CapabilityError,
  ChatError
} from 'meloqui';

async function chat(message: string) {
  try {
    return await client.chat(message);
  } catch (error) {
    if (error instanceof RateLimitError) {
      // Exponential backoff (sleep = ms => new Promise(r => setTimeout(r, ms)))
      await sleep(5000);
      return await client.chat(message);
    }

    if (error instanceof AuthenticationError) {
      throw new Error('Invalid API key - check your configuration');
    }

    if (error instanceof TimeoutError) {
      // Try with longer timeout
      return await client.chat(message, { timeout: 60000 });
    }

    if (error instanceof CapabilityError) {
      console.warn(`${error.capability} not supported, using fallback`);
      // Fall back to text-only
    }

    if (error instanceof ChatError) {
      // Generic handling for other chat errors
      console.error(`Chat failed: ${error.message}`);
    }

    throw error;
  }
}

Using isRetryable

typescript
import { ChatError } from 'meloqui';

async function chatWithRetry(message: string, maxRetries = 3) {
  let lastError: Error | undefined;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await client.chat(message);
    } catch (error) {
      lastError = error as Error;

      if (error instanceof ChatError && error.isRetryable) {
        const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
        console.log(`Retry ${attempt + 1} after ${delay}ms`);
        await sleep(delay);
        continue;
      }

      // Non-retryable error, throw immediately
      throw error;
    }
  }

  throw lastError;
}

Automatic Retry

Let Meloqui handle retries automatically:

typescript
const client = new ChatClient({
  provider: 'openai',
  model: 'gpt-4o',
  retryConfig: {
    maxAttempts: 3,
    initialBackoffMs: 1000,
    maxBackoffMs: 10000,
    backoffMultiplier: 2
  }
});

// Retryable errors are automatically retried
const response = await client.chat('Hello');

Retry Callback

Monitor retry attempts:

typescript
import { RetryManager } from 'meloqui';

const retryManager = new RetryManager(
  {
    maxAttempts: 3,
    initialBackoffMs: 1000,
    maxBackoffMs: 10000,
    backoffMultiplier: 2
  },
  (attempt, error, delayMs) => {
    console.log(`Retry ${attempt}: ${error.message} (waiting ${delayMs}ms)`);
  }
);

Common Scenarios

Vision Fallback

Handle providers that don't support vision:

typescript
import { CapabilityError } from 'meloqui';

async function analyzeImage(imagePath: string, prompt: string) {
  try {
    return await client.chat({
      role: 'user',
      content: [
        { type: 'image', image: imagePath },
        { type: 'text', text: prompt }
      ]
    });
  } catch (error) {
    if (error instanceof CapabilityError) {
      // Provider doesn't support vision, use text description
      return await client.chat(`[Image: ${imagePath}] ${prompt}`);
    }
    throw error;
  }
}

Storage Error Recovery

typescript
import { StorageError, IStorage } from 'meloqui';

async function loadConversation(storage: IStorage, id: string) {
  try {
    return await storage.load(id);
  } catch (error) {
    if (error instanceof StorageError) {
      console.warn(`Storage error: ${error.operation} failed for ${error.conversationId}`);
      // Return empty history as fallback
      return [];
    }
    throw error;
  }
}

RAG Graceful Degradation

typescript
const client = new ChatClient({
  provider: 'openai',
  model: 'gpt-4o',
  documentStore: store,
  ragOptions: {
    failOnError: false, // Don't throw on RAG failures
    onError: (error) => {
      console.warn('RAG failed, continuing without context:', error.message);
    }
  }
});

Best Practices

1. Always Catch ChatError

typescript
try {
  await client.chat(message);
} catch (error) {
  if (error instanceof ChatError) {
    // Handle Meloqui errors
  } else {
    // Handle other errors (network, etc.)
    throw error;
  }
}

2. Log Original Errors

typescript
if (error instanceof ChatError && error.originalError) {
  console.error('Original error:', error.originalError);
}

3. Use Automatic Retry for Production

typescript
// Let the SDK handle transient failures
const client = new ChatClient({
  provider: 'openai',
  model: 'gpt-4o',
  retryConfig: {
    maxAttempts: 3,
    initialBackoffMs: 1000,
    maxBackoffMs: 30000,
    backoffMultiplier: 2
  }
});

4. Handle Provider-Specific Errors

typescript
if (error instanceof ChatError) {
  switch (error.provider) {
    case 'openai':
      // OpenAI-specific handling
      break;
    case 'anthropic':
      // Anthropic-specific handling
      break;
  }
}

5. User-Friendly Messages

typescript
function getUserMessage(error: ChatError): string {
  if (error instanceof RateLimitError) {
    return 'Too many requests. Please wait a moment.';
  }
  if (error instanceof AuthenticationError) {
    return 'Authentication failed. Please check your API key.';
  }
  if (error instanceof TimeoutError) {
    return 'Request timed out. Please try again.';
  }
  return 'An error occurred. Please try again later.';
}

Next Steps

Released under the MIT License.