Bot WA AI Error Handling

Cara handle error pada bot AI WhatsApp. Graceful fallback, retry logic, user-friendly errors. Tutorial lengkap!

Bot WA AI Error Handling
Bot WA AI Error Handling

Error pasti terjadi - handle dengan baik!

Bot yang baik tidak crash saat error, tapi handle gracefully, retry jika perlu, dan tetap helpful ke customer.


Jenis Error

⚠️ KATEGORI ERROR:

1. API ERRORS
   • Rate limit exceeded
   • Timeout
   • Invalid API key
   • Model overloaded

2. NETWORK ERRORS
   • Connection timeout
   • DNS failure
   • SSL errors

3. WHATSAPP ERRORS
   • Session disconnected
   • Media download failed
   • Send message failed

4. LOGIC ERRORS
   • Invalid JSON response
   • Missing required data
   • Unexpected AI output

5. EXTERNAL ERRORS
   • Database down
   • Third-party API failed
   • Webhook timeout

Basic Error Handling

javascript

class BotError extends Error {
    constructor(message, code, recoverable = true) {
        super(message);
        this.code = code;
        this.recoverable = recoverable;
        this.timestamp = new Date();
    }
}

const ERROR_CODES = {
    API_RATE_LIMIT: 'API_RATE_LIMIT',
    API_TIMEOUT: 'API_TIMEOUT',
    API_ERROR: 'API_ERROR',
    WA_DISCONNECTED: 'WA_DISCONNECTED',
    WA_SEND_FAILED: 'WA_SEND_FAILED',
    DB_ERROR: 'DB_ERROR',
    INVALID_RESPONSE: 'INVALID_RESPONSE',
    UNKNOWN: 'UNKNOWN'
};

async function safeExecute(fn, context = {}) {
    try {
        return await fn();
    } catch (error) {
        return handleError(error, context);
    }
}

function handleError(error, context) {
    // Classify error
    const classifiedError = classifyError(error);
    
    // Log error
    logError(classifiedError, context);
    
    // Determine action
    if (classifiedError.recoverable) {
        return getRecoveryAction(classifiedError);
    }
    
    // Alert for critical errors
    if (isCriticalError(classifiedError)) {
        alertAdmin(classifiedError, context);
    }
    
    return getUserFriendlyMessage(classifiedError);
}

Error Classification

javascript

function classifyError(error) {
    const message = error.message?.toLowerCase() || '';
    const code = error.code || error.status;
    
    // Rate limit
    if (code === 429 || message.includes('rate limit')) {
        return new BotError(
            'Rate limit exceeded',
            ERROR_CODES.API_RATE_LIMIT,
            true
        );
    }
    
    // Timeout
    if (code === 'ETIMEDOUT' || code === 'ECONNABORTED' || message.includes('timeout')) {
        return new BotError(
            'Request timeout',
            ERROR_CODES.API_TIMEOUT,
            true
        );
    }
    
    // OpenAI specific
    if (error.type === 'insufficient_quota') {
        return new BotError(
            'API quota exceeded',
            ERROR_CODES.API_ERROR,
            false
        );
    }
    
    if (error.type === 'server_error' || code >= 500) {
        return new BotError(
            'API server error',
            ERROR_CODES.API_ERROR,
            true
        );
    }
    
    // WhatsApp
    if (message.includes('not connected') || message.includes('disconnected')) {
        return new BotError(
            'WhatsApp disconnected',
            ERROR_CODES.WA_DISCONNECTED,
            true
        );
    }
    
    // Default
    return new BotError(
        error.message || 'Unknown error',
        ERROR_CODES.UNKNOWN,
        false
    );
}

Retry Logic

javascript

async function withRetry(fn, options = {}) {
    const {
        maxRetries = 3,
        baseDelay = 1000,
        maxDelay = 30000,
        exponential = true,
        retryOn = [ERROR_CODES.API_RATE_LIMIT, ERROR_CODES.API_TIMEOUT, ERROR_CODES.API_ERROR]
    } = options;
    
    let lastError;
    
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            return await fn();
        } catch (error) {
            lastError = classifyError(error);
            
            // Check if should retry
            if (!retryOn.includes(lastError.code)) {
                throw lastError;
            }
            
            if (attempt === maxRetries) {
                throw lastError;
            }
            
            // Calculate delay
            let delay = exponential 
                ? Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay)
                : baseDelay;
            
            // Add jitter
            delay += Math.random() * 1000;
            
            console.log(`Retry ${attempt}/${maxRetries} after ${delay}ms`);
            await sleep(delay);
        }
    }
    
    throw lastError;
}

// Usage
async function callAIWithRetry(messages) {
    return await withRetry(
        () => openai.chat.completions.create({
            model: 'gpt-4o-mini',
            messages,
            timeout: 30000
        }),
        {
            maxRetries: 3,
            baseDelay: 2000,
            exponential: true
        }
    );
}

Graceful Fallback

javascript

async function getAIResponseWithFallback(userId, message) {
    const fallbackResponses = {
        general: 'Maaf kak, sistem sedang sibuk. Bisa coba lagi dalam beberapa saat? 🙏',
        rateLimit: 'Wah, lagi ramai banget nih kak! Tunggu sebentar ya, sekitar 1 menit 😊',
        maintenance: 'Sistem sedang maintenance sebentar kak. Coba lagi dalam 5 menit ya!',
        humanHandover: 'Aku hubungkan dengan tim CS ya kak. Mohon tunggu sebentar... 🙏'
    };
    
    try {
        // Try primary model
        return await callAIWithRetry([
            { role: 'system', content: SYSTEM_PROMPT },
            { role: 'user', content: message }
        ]);
        
    } catch (primaryError) {
        console.error('Primary model failed:', primaryError);
        
        // Fallback 1: Try cheaper model
        try {
            console.log('Trying fallback model...');
            const response = await openai.chat.completions.create({
                model: 'gpt-3.5-turbo',
                messages: [
                    { role: 'system', content: SYSTEM_PROMPT },
                    { role: 'user', content: message }
                ],
                timeout: 15000
            });
            return response.choices[0].message.content;
            
        } catch (fallbackError) {
            console.error('Fallback model failed:', fallbackError);
        }
        
        // Fallback 2: Try keyword-based response
        const keywordResponse = getKeywordResponse(message);
        if (keywordResponse) {
            return keywordResponse;
        }
        
        // Fallback 3: Human handover
        if (primaryError.code === ERROR_CODES.API_RATE_LIMIT) {
            await notifyHumanAgent(userId, message, 'AI rate limited');
            return fallbackResponses.humanHandover;
        }
        
        // Fallback 4: Generic message
        return fallbackResponses.general;
    }
}

User-Friendly Error Messages

javascript

const USER_ERROR_MESSAGES = {
    [ERROR_CODES.API_RATE_LIMIT]: {
        message: 'Waduh, lagi rame banget nih kak! 😅\nCoba lagi dalam 1-2 menit ya.',
        action: 'retry_later',
        retryAfter: 60
    },
    
    [ERROR_CODES.API_TIMEOUT]: {
        message: 'Hmm, koneksinya lagi lambat nih kak.\nCoba kirim ulang pesannya ya! 🙏',
        action: 'retry_now'
    },
    
    [ERROR_CODES.WA_DISCONNECTED]: {
        message: 'Ada gangguan koneksi sebentar kak.\nPesan kakak sudah kami terima, akan dibalas segera!',
        action: 'queue_message'
    },
    
    [ERROR_CODES.DB_ERROR]: {
        message: 'Maaf kak, ada kendala teknis.\nTim kami sudah diberitahu dan sedang diperbaiki 🔧',
        action: 'alert_admin'
    },
    
    [ERROR_CODES.UNKNOWN]: {
        message: 'Maaf kak, ada kendala yang tidak terduga 😔\nBisa coba lagi atau ketik ADMIN untuk bantuan.',
        action: 'offer_human'
    }
};

function getUserFriendlyMessage(error) {
    const errorInfo = USER_ERROR_MESSAGES[error.code] || USER_ERROR_MESSAGES[ERROR_CODES.UNKNOWN];
    return errorInfo.message;
}

Circuit Breaker

javascript

class CircuitBreaker {
    constructor(options = {}) {
        this.failureThreshold = options.failureThreshold || 5;
        this.resetTimeout = options.resetTimeout || 60000;
        this.failures = 0;
        this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
        this.lastFailureTime = null;
    }
    
    async execute(fn) {
        if (this.state === 'OPEN') {
            // Check if should try again
            if (Date.now() - this.lastFailureTime > this.resetTimeout) {
                this.state = 'HALF_OPEN';
            } else {
                throw new BotError('Service temporarily unavailable', 'CIRCUIT_OPEN', true);
            }
        }
        
        try {
            const result = await fn();
            this.onSuccess();
            return result;
        } catch (error) {
            this.onFailure();
            throw error;
        }
    }
    
    onSuccess() {
        this.failures = 0;
        this.state = 'CLOSED';
    }
    
    onFailure() {
        this.failures++;
        this.lastFailureTime = Date.now();
        
        if (this.failures >= this.failureThreshold) {
            this.state = 'OPEN';
            console.log('Circuit breaker OPEN - service failing');
        }
    }
    
    getState() {
        return {
            state: this.state,
            failures: this.failures,
            lastFailure: this.lastFailureTime
        };
    }
}

// Usage
const aiCircuitBreaker = new CircuitBreaker({
    failureThreshold: 5,
    resetTimeout: 60000
});

async function callAIWithCircuitBreaker(messages) {
    return await aiCircuitBreaker.execute(async () => {
        return await openai.chat.completions.create({
            model: 'gpt-4o-mini',
            messages
        });
    });
}

Error Logging

javascript

async function logError(error, context) {
    const errorLog = {
        timestamp: new Date(),
        code: error.code,
        message: error.message,
        stack: error.stack,
        recoverable: error.recoverable,
        context: {
            userId: context.userId,
            message: context.message,
            sessionId: context.sessionId,
            endpoint: context.endpoint
        },
        environment: {
            nodeVersion: process.version,
            memory: process.memoryUsage(),
            uptime: process.uptime()
        }
    };
    
    // Log to console
    console.error('Error:', JSON.stringify(errorLog, null, 2));
    
    // Log to database
    await db.errorLogs.insertOne(errorLog);
    
    // Send to monitoring service (e.g., Sentry)
    if (process.env.SENTRY_DSN) {
        Sentry.captureException(error, {
            extra: errorLog.context
        });
    }
}

// Alert for critical errors
async function alertAdmin(error, context) {
    const alertMessage = `🚨 CRITICAL ERROR

Code: ${error.code}
Message: ${error.message}
User: ${context.userId}
Time: ${new Date().toISOString()}

Immediate attention required!`;

    // Send to admin WhatsApp
    await sendWhatsApp(process.env.ADMIN_PHONE, alertMessage);
    
    // Send to Slack/Discord
    await sendSlackAlert(alertMessage);
}

Full Implementation

javascript

async function handleMessage(userId, message) {
    const context = { userId, message, startTime: Date.now() };
    
    try {
        // Validate input
        if (!message || message.trim().length === 0) {
            return 'Hai kak! Ada yang bisa dibantu? 😊';
        }
        
        // Get AI response with all safeguards
        const response = await safeExecute(
            () => getAIResponseWithFallback(userId, message),
            context
        );
        
        // Track success
        await trackSuccess(context);
        
        return response;
        
    } catch (error) {
        // This should rarely happen with all fallbacks
        const friendlyMessage = getUserFriendlyMessage(classifyError(error));
        
        // Queue for human follow-up
        await queueForHumanReview({
            userId,
            message,
            error: error.message,
            timestamp: new Date()
        });
        
        return friendlyMessage;
    }
}

Best Practices

DO ✅

- Classify errors properly
- Implement retry with backoff
- Use circuit breaker
- Provide user-friendly messages
- Log comprehensively
- Have multiple fallbacks
- Alert on critical errors

DON'T ❌

- Show technical errors to users
- Retry infinitely
- Ignore error patterns
- Log sensitive data
- Single point of failure
- Silent failures

FAQ

Berapa kali retry yang ideal?

3 kali dengan exponential backoff. Lebih dari itu, fallback ke alternatif.

Perlu human fallback?

Ya, selalu. AI bukan 100% reliable, human backup essential.


Kesimpulan

Good error handling = Reliable bot!

No Error HandlingWith Error Handling
CrashesGraceful fallback
User frustratedUser informed
Silent failuresLogged & alerted

Build Robust Bot →


Artikel Terkait