374 lines
10 KiB
TypeScript
374 lines
10 KiB
TypeScript
import Anthropic from '@anthropic-ai/sdk';
|
|
|
|
const anthropic = new Anthropic({
|
|
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
});
|
|
|
|
// Example 1: Basic error handling
|
|
async function basicErrorHandling(prompt: string) {
|
|
try {
|
|
const message = await anthropic.messages.create({
|
|
model: 'claude-sonnet-4-5-20250929',
|
|
max_tokens: 1024,
|
|
messages: [{ role: 'user', content: prompt }],
|
|
});
|
|
|
|
return message;
|
|
} catch (error) {
|
|
if (error instanceof Anthropic.APIError) {
|
|
console.error(`API Error [${error.status}]:`, error.message);
|
|
console.error('Error type:', error.type);
|
|
console.error('Error details:', error.error);
|
|
|
|
// Handle specific error types
|
|
switch (error.status) {
|
|
case 400:
|
|
console.error('Invalid request. Check your parameters.');
|
|
break;
|
|
case 401:
|
|
console.error('Authentication failed. Check your API key.');
|
|
break;
|
|
case 403:
|
|
console.error('Permission denied. Check your account tier.');
|
|
break;
|
|
case 404:
|
|
console.error('Resource not found. Check the endpoint.');
|
|
break;
|
|
case 429:
|
|
console.error('Rate limit exceeded. Implement retry logic.');
|
|
break;
|
|
case 500:
|
|
console.error('Server error. Retry with exponential backoff.');
|
|
break;
|
|
case 529:
|
|
console.error('API overloaded. Retry later.');
|
|
break;
|
|
default:
|
|
console.error('Unexpected error occurred.');
|
|
}
|
|
} else {
|
|
console.error('Non-API error:', error);
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Example 2: Rate limit handler with retry
|
|
async function handleRateLimits(
|
|
requestFn: () => Promise<any>,
|
|
maxRetries = 3,
|
|
baseDelay = 1000
|
|
): Promise<any> {
|
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
try {
|
|
return await requestFn();
|
|
} catch (error) {
|
|
if (error instanceof Anthropic.APIError && error.status === 429) {
|
|
// Check retry-after header
|
|
const retryAfter = error.response?.headers?.['retry-after'];
|
|
const delay = retryAfter
|
|
? parseInt(retryAfter) * 1000
|
|
: baseDelay * Math.pow(2, attempt);
|
|
|
|
if (attempt < maxRetries - 1) {
|
|
console.warn(`Rate limited. Retrying in ${delay}ms... (Attempt ${attempt + 1}/${maxRetries})`);
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
throw new Error('Max retries exceeded');
|
|
}
|
|
|
|
// Example 3: Comprehensive error handler
|
|
class APIErrorHandler {
|
|
private maxRetries: number;
|
|
private baseDelay: number;
|
|
private onError?: (error: Error) => void;
|
|
|
|
constructor(options: {
|
|
maxRetries?: number;
|
|
baseDelay?: number;
|
|
onError?: (error: Error) => void;
|
|
} = {}) {
|
|
this.maxRetries = options.maxRetries || 3;
|
|
this.baseDelay = options.baseDelay || 1000;
|
|
this.onError = options.onError;
|
|
}
|
|
|
|
async execute<T>(requestFn: () => Promise<T>): Promise<T> {
|
|
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
try {
|
|
return await requestFn();
|
|
} catch (error) {
|
|
if (this.onError) {
|
|
this.onError(error);
|
|
}
|
|
|
|
if (error instanceof Anthropic.APIError) {
|
|
if (this.shouldRetry(error) && attempt < this.maxRetries - 1) {
|
|
const delay = this.calculateDelay(error, attempt);
|
|
console.warn(`Retrying after ${delay}ms... (${attempt + 1}/${this.maxRetries})`);
|
|
await this.sleep(delay);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
throw this.enhanceError(error);
|
|
}
|
|
}
|
|
|
|
throw new Error('Max retries exceeded');
|
|
}
|
|
|
|
private shouldRetry(error: Anthropic.APIError): boolean {
|
|
// Retry on rate limits, server errors, and overload
|
|
return error.status === 429 || error.status === 500 || error.status === 529;
|
|
}
|
|
|
|
private calculateDelay(error: Anthropic.APIError, attempt: number): number {
|
|
// Use retry-after header if available
|
|
const retryAfter = error.response?.headers?.['retry-after'];
|
|
if (retryAfter) {
|
|
return parseInt(retryAfter) * 1000;
|
|
}
|
|
|
|
// Exponential backoff
|
|
return this.baseDelay * Math.pow(2, attempt);
|
|
}
|
|
|
|
private enhanceError(error: any): Error {
|
|
if (error instanceof Anthropic.APIError) {
|
|
const enhancedError = new Error(`Claude API Error: ${error.message}`);
|
|
(enhancedError as any).originalError = error;
|
|
(enhancedError as any).status = error.status;
|
|
(enhancedError as any).type = error.type;
|
|
return enhancedError;
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
private sleep(ms: number): Promise<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
}
|
|
|
|
// Example 4: Streaming error handling
|
|
async function streamWithErrorHandling(prompt: string) {
|
|
try {
|
|
const stream = anthropic.messages.stream({
|
|
model: 'claude-sonnet-4-5-20250929',
|
|
max_tokens: 1024,
|
|
messages: [{ role: 'user', content: prompt }],
|
|
});
|
|
|
|
let hasError = false;
|
|
|
|
stream.on('error', (error) => {
|
|
hasError = true;
|
|
console.error('Stream error:', error);
|
|
|
|
if (error instanceof Anthropic.APIError) {
|
|
console.error(`Status: ${error.status}`);
|
|
console.error(`Type: ${error.type}`);
|
|
}
|
|
|
|
// Implement fallback or retry logic here
|
|
});
|
|
|
|
stream.on('abort', (error) => {
|
|
console.warn('Stream aborted:', error);
|
|
});
|
|
|
|
stream.on('text', (text) => {
|
|
if (!hasError) {
|
|
process.stdout.write(text);
|
|
}
|
|
});
|
|
|
|
await stream.finalMessage();
|
|
|
|
if (hasError) {
|
|
throw new Error('Stream completed with errors');
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to complete stream:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Example 5: Validation errors
|
|
function validateRequest(params: {
|
|
messages: any[];
|
|
max_tokens?: number;
|
|
model?: string;
|
|
}): { valid: boolean; errors: string[] } {
|
|
const errors: string[] = [];
|
|
|
|
if (!Array.isArray(params.messages) || params.messages.length === 0) {
|
|
errors.push('Messages must be a non-empty array');
|
|
}
|
|
|
|
if (params.max_tokens && (params.max_tokens < 1 || params.max_tokens > 8192)) {
|
|
errors.push('max_tokens must be between 1 and 8192');
|
|
}
|
|
|
|
if (params.model && !params.model.startsWith('claude-')) {
|
|
errors.push('Invalid model name');
|
|
}
|
|
|
|
for (const [index, message] of params.messages.entries()) {
|
|
if (!message.role || !['user', 'assistant'].includes(message.role)) {
|
|
errors.push(`Message ${index}: Invalid role. Must be "user" or "assistant"`);
|
|
}
|
|
|
|
if (!message.content) {
|
|
errors.push(`Message ${index}: Missing content`);
|
|
}
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors,
|
|
};
|
|
}
|
|
|
|
// Example 6: Circuit breaker pattern
|
|
class CircuitBreaker {
|
|
private failures: number = 0;
|
|
private lastFailureTime: number = 0;
|
|
private state: 'closed' | 'open' | 'half-open' = 'closed';
|
|
private readonly threshold: number;
|
|
private readonly timeout: number;
|
|
|
|
constructor(options: { threshold?: number; timeout?: number } = {}) {
|
|
this.threshold = options.threshold || 5;
|
|
this.timeout = options.timeout || 60000; // 1 minute
|
|
}
|
|
|
|
async execute<T>(requestFn: () => Promise<T>): Promise<T> {
|
|
if (this.state === 'open') {
|
|
if (Date.now() - this.lastFailureTime > this.timeout) {
|
|
console.log('Circuit breaker: Transitioning to half-open');
|
|
this.state = 'half-open';
|
|
} else {
|
|
throw new Error('Circuit breaker is open. Service unavailable.');
|
|
}
|
|
}
|
|
|
|
try {
|
|
const result = await requestFn();
|
|
|
|
// Success - reset failures
|
|
if (this.state === 'half-open') {
|
|
console.log('Circuit breaker: Transitioning to closed');
|
|
this.state = 'closed';
|
|
}
|
|
this.failures = 0;
|
|
|
|
return result;
|
|
} catch (error) {
|
|
this.failures++;
|
|
this.lastFailureTime = Date.now();
|
|
|
|
if (this.failures >= this.threshold) {
|
|
console.error(`Circuit breaker: Opening after ${this.failures} failures`);
|
|
this.state = 'open';
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
getState(): { state: string; failures: number } {
|
|
return {
|
|
state: this.state,
|
|
failures: this.failures,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Example 7: Usage with all patterns
|
|
async function robustAPICall(prompt: string) {
|
|
const errorHandler = new APIErrorHandler({
|
|
maxRetries: 3,
|
|
baseDelay: 1000,
|
|
onError: (error) => {
|
|
console.error('Error logged:', error);
|
|
// Could send to monitoring service here
|
|
},
|
|
});
|
|
|
|
const circuitBreaker = new CircuitBreaker({
|
|
threshold: 5,
|
|
timeout: 60000,
|
|
});
|
|
|
|
try {
|
|
const validation = validateRequest({
|
|
messages: [{ role: 'user', content: prompt }],
|
|
max_tokens: 1024,
|
|
model: 'claude-sonnet-4-5-20250929',
|
|
});
|
|
|
|
if (!validation.valid) {
|
|
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);
|
|
}
|
|
|
|
const result = await circuitBreaker.execute(() =>
|
|
errorHandler.execute(() =>
|
|
anthropic.messages.create({
|
|
model: 'claude-sonnet-4-5-20250929',
|
|
max_tokens: 1024,
|
|
messages: [{ role: 'user', content: prompt }],
|
|
})
|
|
)
|
|
);
|
|
|
|
return result;
|
|
} catch (error) {
|
|
console.error('Robust API call failed:', error);
|
|
console.error('Circuit breaker state:', circuitBreaker.getState());
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Run examples
|
|
if (require.main === module) {
|
|
console.log('=== Error Handling Examples ===\n');
|
|
|
|
// Test basic error handling
|
|
basicErrorHandling('Hello, Claude!')
|
|
.then(() => {
|
|
console.log('\n=== Testing Rate Limit Handler ===\n');
|
|
return handleRateLimits(() =>
|
|
anthropic.messages.create({
|
|
model: 'claude-sonnet-4-5-20250929',
|
|
max_tokens: 1024,
|
|
messages: [{ role: 'user', content: 'Test message' }],
|
|
})
|
|
);
|
|
})
|
|
.then(() => {
|
|
console.log('\n=== Testing Robust API Call ===\n');
|
|
return robustAPICall('What is 2+2?');
|
|
})
|
|
.catch(console.error);
|
|
}
|
|
|
|
export {
|
|
basicErrorHandling,
|
|
handleRateLimits,
|
|
APIErrorHandler,
|
|
streamWithErrorHandling,
|
|
validateRequest,
|
|
CircuitBreaker,
|
|
robustAPICall,
|
|
};
|