Bot WA AI Error Handling
Cara handle error pada bot AI WhatsApp. Graceful fallback, retry logic, user-friendly errors. Tutorial lengkap!
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 timeoutBasic 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 errorsDON'T ❌
- Show technical errors to users
- Retry infinitely
- Ignore error patterns
- Log sensitive data
- Single point of failure
- Silent failuresFAQ
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 Handling | With Error Handling |
|---|---|
| Crashes | Graceful fallback |
| User frustrated | User informed |
| Silent failures | Logged & alerted |