Error Handling
Complete guide for handling MONCRENEAU API errors robustly.
1. HTTP Error Codes
The API uses standard HTTP codes:
| Code | Meaning | Action |
|---|---|---|
| 2xx | Success | Continue |
| 4xx | Client error | Fix the request |
| 5xx | Server error | Retry with backoff |
4xx Errors (Client Errors)
try {
const response = await api.post('/appointments', data);
} catch (error) {
if (error.response?.status >= 400 && error.response?.status < 500) {
// Client error - DO NOT RETRY
const errorData = error.response.data;
switch (errorData.error.code) {
case 'VALIDATION_ERROR':
console.error('Invalid data:', errorData.error.details);
// Display errors to user
break;
case 'INSUFFICIENT_CREDITS':
console.error('Insufficient credits');
// Redirect to recharge page
break;
case 'SLOT_NOT_AVAILABLE':
console.error('Slot already booked');
// Suggest other slots
break;
case 'UNAUTHORIZED':
console.error('Invalid API key');
// Check configuration
break;
default:
console.error('Client error:', errorData);
}
}
}
5xx Errors (Server Errors)
async function callApiWithRetry(fn, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
const status = error.response?.status;
if (status >= 500 || !status) {
// Server or network error - RETRY
if (attempt < maxAttempts) {
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await sleep(delay);
continue;
}
}
// Last attempt or non-retryable error
throw error;
}
}
}
// Usage
const appointment = await callApiWithRetry(() =>
api.post('/appointments', data)
);
2. Error Structure
All errors follow this format:
{
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Insufficient credits to create appointment",
"details": {
"required": 1,
"available": 0
}
},
"timestamp": "2024-02-01T10:30:00Z",
"path": "/v1/appointments"
}
Parsing Errors
function parseApiError(error) {
if (!error.response) {
return {
code: 'NETWORK_ERROR',
message: 'Unable to contact server',
retryable: true
};
}
const status = error.response.status;
const data = error.response.data;
return {
code: data?.error?.code || 'UNKNOWN_ERROR',
message: data?.error?.message || error.message,
details: data?.error?.details,
status: status,
retryable: status >= 500 || status === 429
};
}
// Usage
try {
await api.post('/appointments', data);
} catch (error) {
const parsedError = parseApiError(error);
if (parsedError.retryable) {
console.log('Error is retryable');
}
console.error(`[${parsedError.code}] ${parsedError.message}`);
}
3. Retry Patterns
Exponential Backoff
async function exponentialBackoff(fn, options = {}) {
const {
maxAttempts = 3,
baseDelay = 1000,
maxDelay = 30000,
factor = 2
} = options;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
const shouldRetry =
attempt < maxAttempts &&
isRetryableError(error);
if (!shouldRetry) throw error;
const delay = Math.min(
baseDelay * Math.pow(factor, attempt - 1),
maxDelay
);
console.log(`Retry ${attempt}/${maxAttempts} after ${delay}ms`);
await sleep(delay);
}
}
}
function isRetryableError(error) {
const status = error.response?.status;
return (
!status || // Network error
status >= 500 || // Server error
status === 429 || // Rate limit
status === 408 // Timeout
);
}
// Usage
const result = await exponentialBackoff(
() => api.post('/appointments', data),
{ maxAttempts: 5, baseDelay: 2000 }
);
Circuit Breaker
Temporarily stops requests after multiple failures.
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.resetTimeout = options.resetTimeout || 60000; // 1 minute
this.failures = 0;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
}
async execute(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
}
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++;
if (this.failures >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.resetTimeout;
console.error(`Circuit breaker OPEN for ${this.resetTimeout}ms`);
}
}
}
// Usage
const breaker = new CircuitBreaker({
failureThreshold: 5,
resetTimeout: 60000
});
try {
const result = await breaker.execute(() =>
api.post('/appointments', data)
);
} catch (error) {
console.error('Request failed or circuit open:', error);
}
4. Rate Limiting (429)
Handle the 100 requests/minute limit.
const pThrottle = require('p-throttle');
// Limit to 80 req/min (safety margin)
const throttle = pThrottle({
limit: 80,
interval: 60000,
strict: true
});
const createAppointment = throttle(async (data) => {
return api.post('/appointments', data);
});
// Handle 429 with Retry-After
async function handleRateLimit(fn) {
try {
return await fn();
} catch (error) {
if (error.response?.status === 429) {
const retryAfter = error.response.headers['retry-after'];
const delay = retryAfter ? parseInt(retryAfter) * 1000 : 60000;
console.log(`Rate limited. Retrying in ${delay}ms`);
await sleep(delay);
return await fn();
}
throw error;
}
}
// Usage
const appointment = await handleRateLimit(() =>
createAppointment(data)
);
5. Timeouts
Always define a timeout.
const axios = require('axios');
const api = axios.create({
baseURL: 'https://api.moncreneau.com/v1',
timeout: 30000, // 30 seconds
headers: {
'Authorization': `Bearer ${process.env.MONCRENEAU_API_KEY}`
}
});
// Request-specific timeout
try {
const response = await api.post('/appointments', data, {
timeout: 10000 // 10 seconds for this request
});
} catch (error) {
if (error.code === 'ECONNABORTED') {
console.error('Request timeout');
// Retry with longer timeout
}
}
6. Structured Logging
Log all errors with context.
const winston = require('winston');
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
async function createAppointment(data) {
const requestId = generateRequestId();
try {
logger.info('Creating appointment', {
requestId,
organizationId: data.organizationId,
departmentId: data.departmentId
});
const response = await api.post('/appointments', data);
logger.info('Appointment created', {
requestId,
appointmentId: response.data.id,
status: response.data.status
});
return response.data;
} catch (error) {
const parsedError = parseApiError(error);
logger.error('Failed to create appointment', {
requestId,
errorCode: parsedError.code,
errorMessage: parsedError.message,
status: parsedError.status,
data: data,
stack: error.stack
});
throw error;
}
}
7. Alerts
Configure alerts for critical errors.
const { WebClient } = require('@slack/web-api');
const slack = new WebClient(process.env.SLACK_TOKEN);
async function sendAlert(error, context) {
if (error.response?.status >= 500) {
await slack.chat.postMessage({
channel: '#tech-alerts',
text: `API Error`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*API Error*\n*Code:* ${error.code}\n*Message:* ${error.message}\n*Context:* ${JSON.stringify(context)}`
}
}
]
});
}
}
// Usage
try {
await api.post('/appointments', data);
} catch (error) {
await sendAlert(error, {
action: 'create_appointment',
organizationId: data.organizationId
});
throw error;
}
8. Fallback Strategies
Graceful Degradation
async function createAppointmentWithFallback(data) {
try {
// Normal attempt
return await api.post('/appointments', data);
} catch (error) {
if (error.response?.status >= 500) {
// Fallback: save locally
console.warn('API unavailable, saving to local queue');
await saveToLocalQueue(data);
return {
id: 'pending_' + Date.now(),
status: 'queued',
message: 'Appointment pending synchronization'
};
}
throw error;
}
}
// Worker to sync queue
setInterval(async () => {
const pendingAppointments = await getLocalQueue();
for (const appointment of pendingAppointments) {
try {
const created = await api.post('/appointments', appointment);
await removeFromLocalQueue(appointment.id);
console.log(`Synced appointment ${created.data.id}`);
} catch (error) {
console.error(`Failed to sync ${appointment.id}`);
}
}
}, 60000); // Every minute
9. Dead Letter Queue
For events that fail definitively.
const Bull = require('bull');
const appointmentQueue = new Bull('appointments');
const deadLetterQueue = new Bull('appointments-dlq');
appointmentQueue.process(async (job) => {
try {
return await api.post('/appointments', job.data);
} catch (error) {
if (job.attemptsMade >= 5) {
// Definitive failure after 5 attempts
await deadLetterQueue.add({
originalJob: job.data,
error: error.message,
attempts: job.attemptsMade,
failedAt: new Date()
});
// Alert team
await sendAlert(error, { jobId: job.id });
}
throw error;
}
});
// Monitor DLQ
setInterval(async () => {
const dlqCount = await deadLetterQueue.count();
if (dlqCount > 10) {
console.error(`⚠️ ${dlqCount} items in dead letter queue`);
}
}, 300000); // Every 5 minutes
Error Handling Checklist
- All API calls in try/catch
- Retry with exponential backoff for 5xx
- No retry for 4xx (except 429)
- Circuit breaker configured
- Timeouts defined (30s max)
- Client-side rate limiting (< 100/min)
- Structured logs with context
- Alerts on critical errors
- Dead letter queue for failures
- Fallback strategy defined