Error Handling
Handle API errors gracefully with consistent error responses and proper retry logic.
Overview
FormaMail API uses conventional HTTP response codes and returns structured error responses in JSON format. All errors follow a consistent schema to make error handling predictable and straightforward.
Error Response Format
All errors return this consistent structure:
{
"statusCode": 400,
"code": "ERR_VALID_001",
"message": "Validation failed",
"timestamp": "2025-11-07T10:30:00.000Z",
"path": "/api/emails/send",
"relatedInfo": {
"errors": [
{
"field": "to",
"message": "At least one recipient is required"
}
]
}
}Response Fields
| Field | Type | Description |
|---|---|---|
statusCode | number | HTTP status code |
code | string | Error code (e.g., ERR_VALID_001) |
message | string | Human-readable error message |
timestamp | string | ISO 8601 timestamp |
path | string | API endpoint that generated error |
relatedInfo | object | Additional context (optional) |
HTTP Status Codes
Success Codes (2xx)
| Code | Description |
|---|---|
200 OK | Request successful |
201 Created | Resource created successfully |
204 No Content | Request successful, no content to return |
Client Error Codes (4xx)
| Code | Description | When to Expect |
|---|---|---|
400 Bad Request | Invalid request data | Validation errors, malformed JSON |
401 Unauthorized | Authentication required | Missing or invalid API key |
403 Forbidden | Insufficient permissions | API key lacks required permission |
404 Not Found | Resource doesn’t exist | Template, email, or endpoint not found |
409 Conflict | Resource conflict | Duplicate resource, version mismatch |
422 Unprocessable Entity | Valid syntax but semantic errors | Business logic violation |
429 Too Many Requests | Rate limit exceeded | Too many requests in time window |
Server Error Codes (5xx)
| Code | Description | What to Do |
|---|---|---|
500 Internal Server Error | Unexpected server error | Retry with exponential backoff |
502 Bad Gateway | Upstream service error | Retry after delay |
503 Service Unavailable | Service temporarily down | Check status page, retry later |
504 Gateway Timeout | Request timeout | Retry with longer timeout |
Error Codes
Authentication Errors (ERR_AUTH_xxx)
{
"statusCode": 401,
"code": "ERR_AUTH_001",
"message": "Invalid API key"
}| Code | Message | Solution |
|---|---|---|
ERR_AUTH_001 | Invalid API key | Check API key is correct |
ERR_AUTH_002 | API key has expired | Create new API key |
ERR_AUTH_003 | Insufficient permissions | Check API key permissions |
Validation Errors (ERR_VALID_xxx)
{
"statusCode": 400,
"code": "ERR_VALID_001",
"message": "Validation failed",
"relatedInfo": {
"errors": [
{ "field": "to", "message": "Invalid email format" },
{ "field": "subject", "message": "Subject is required" }
]
}
}| Code | Message |
|---|---|
ERR_VALID_001 | Validation failed |
ERR_VALID_002 | Invalid email format |
ERR_VALID_003 | Required field missing |
Template Errors (ERR_TMPL_xxx)
{
"statusCode": 404,
"code": "ERR_TMPL_002",
"message": "Email template not found",
"relatedInfo": {
"templateId": "tmpl_invalid"
}
}| Code | Message |
|---|---|
ERR_TMPL_001 | Template validation failed |
ERR_TMPL_002 | Template not found |
ERR_TMPL_003 | Template is not published |
ERR_TMPL_004 | Missing required variable |
Email Errors (ERR_EMAIL_xxx)
{
"statusCode": 400,
"code": "ERR_EMAIL_001",
"message": "Invalid recipient email address",
"relatedInfo": {
"email": "invalid-email"
}
}| Code | Message |
|---|---|
ERR_EMAIL_001 | Invalid recipient email |
ERR_EMAIL_002 | Recipient on suppression list |
ERR_EMAIL_003 | Too many recipients |
Quota Errors (ERR_QUOTA_xxx)
{
"statusCode": 429,
"code": "ERR_QUOTA_001",
"message": "Monthly email quota exceeded",
"relatedInfo": {
"limit": 10000,
"used": 10000,
"resetsAt": "2025-12-01T00:00:00Z"
}
}| Code | Message |
|---|---|
ERR_QUOTA_001 | Monthly quota exceeded |
ERR_QUOTA_002 | Daily quota exceeded |
Implementing Error Handling
JavaScript/TypeScript
interface ApiError {
statusCode: number;
code: string;
message: string;
timestamp: string;
path: string;
relatedInfo?: Record<string, any>;
}
async function sendEmail(data: any) {
try {
const response = await fetch('https://api.formamail.com/api/emails/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
const error: ApiError = await response.json();
throw new FormamailError(error);
}
return await response.json();
} catch (error) {
if (error instanceof FormamailError) {
handleFormamailError(error);
} else {
// Network error
console.error('Network error:', error);
}
throw error;
}
}
class FormamailError extends Error {
statusCode: number;
code: string;
relatedInfo?: Record<string, any>;
constructor(apiError: ApiError) {
super(apiError.message);
this.name = 'FormamailError';
this.statusCode = apiError.statusCode;
this.code = apiError.code;
this.relatedInfo = apiError.relatedInfo;
}
}
function handleFormamailError(error: FormamailError) {
switch (error.code) {
case 'ERR_AUTH_001':
// Invalid API key
console.error('Invalid API key. Please check your credentials.');
break;
case 'ERR_QUOTA_001':
// Quota exceeded
const { limit, resetsAt } = error.relatedInfo!;
console.error(`Monthly quota of ${limit} exceeded. Resets at ${resetsAt}`);
break;
case 'ERR_VALID_001':
// Validation errors
const { errors } = error.relatedInfo!;
errors.forEach((err: any) => {
console.error(`${err.field}: ${err.message}`);
});
break;
case 'ERR_TMPL_002':
// Template not found
console.error(`Template not found: ${error.relatedInfo!.templateId}`);
break;
default:
console.error('API error:', error.message);
}
}Python
from typing import Dict, Any, Optional
import requests
class FormamailError(Exception):
def __init__(self, status_code: int, code: str, message: str, related_info: Optional[Dict] = None):
self.status_code = status_code
self.code = code
self.message = message
self.related_info = related_info or {}
super().__init__(self.message)
def send_email(data: Dict[str, Any], api_key: str):
try:
response = requests.post(
'https://api.formamail.com/api/emails/send',
headers={
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json'
},
json=data
)
if not response.ok:
error_data = response.json()
raise FormamailError(
status_code=error_data['statusCode'],
code=error_data['code'],
message=error_data['message'],
related_info=error_data.get('relatedInfo')
)
return response.json()
except FormamailError as e:
handle_formamail_error(e)
raise
except requests.RequestException as e:
print(f"Network error: {e}")
raise
def handle_formamail_error(error: FormamailError):
if error.code == 'ERR_AUTH_001':
print('Invalid API key. Please check your credentials.')
elif error.code == 'ERR_QUOTA_001':
limit = error.related_info.get('limit')
resets_at = error.related_info.get('resetsAt')
print(f'Monthly quota of {limit} exceeded. Resets at {resets_at}')
elif error.code == 'ERR_VALID_001':
errors = error.related_info.get('errors', [])
for err in errors:
print(f"{err['field']}: {err['message']}")
elif error.code == 'ERR_TMPL_002':
template_id = error.related_info.get('templateId')
print(f'Template not found: {template_id}')
else:
print(f'API error: {error.message}')Retry Strategies
Exponential Backoff
async function sendEmailWithRetry(data, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await sendEmail(data);
} catch (error) {
lastError = error;
// Don't retry client errors (4xx except 429)
if (error.statusCode >= 400 && error.statusCode < 500 && error.statusCode !== 429) {
throw error;
}
// Calculate delay with exponential backoff
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
console.log(`Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}Retry Conditions
Retry these errors:
500 Internal Server Error502 Bad Gateway503 Service Unavailable504 Gateway Timeout429 Too Many Requests(with backoff fromRetry-Afterheader)- Network timeouts
- Connection errors
Don’t retry these:
400 Bad Request- Fix request data401 Unauthorized- Fix authentication403 Forbidden- Fix permissions404 Not Found- Resource doesn’t exist409 Conflict- Fix conflict422 Unprocessable Entity- Fix business logic
Rate Limit Headers
Rate limit responses include headers:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1699363200
Retry-After: 60Using Rate Limit Headers
async function sendWithRateLimit(data) {
try {
const response = await fetch('https://api.formamail.com/api/emails/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
// Check rate limit headers
const limit = parseInt(response.headers.get('X-RateLimit-Limit') || '0');
const remaining = parseInt(response.headers.get('X-RateLimit-Remaining') || '0');
const reset = parseInt(response.headers.get('X-RateLimit-Reset') || '0');
console.log(`Rate limit: ${remaining}/${limit} remaining. Resets at ${new Date(reset * 1000)}`);
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
console.log(`Rate limited. Retry after ${retryAfter} seconds`);
throw new Error('Rate limited');
}
if (!response.ok) {
const error = await response.json();
throw new FormamailError(error);
}
return await response.json();
} catch (error) {
console.error('Error sending email:', error);
throw error;
}
}Best Practices
1. Log All Errors
function logError(error, context) {
console.error({
timestamp: new Date().toISOString(),
errorCode: error.code,
statusCode: error.statusCode,
message: error.message,
context,
relatedInfo: error.relatedInfo
});
// Send to error tracking service
errorTracker.captureException(error, { extra: context });
}2. Provide User-Friendly Messages
function getUserMessage(error) {
const messages = {
'ERR_AUTH_001': 'Authentication failed. Please check your API key.',
'ERR_QUOTA_001': 'You have reached your monthly email limit. Please upgrade your plan or wait until next month.',
'ERR_VALID_001': 'Please check your input and try again.',
'ERR_TMPL_002': 'The requested template was not found.',
};
return messages[error.code] || 'An unexpected error occurred. Please try again later.';
}3. Implement Circuit Breaker
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureCount = 0;
this.threshold = threshold;
this.timeout = timeout;
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.failureCount = 0;
this.state = 'closed';
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.threshold) {
this.state = 'open';
this.nextAttempt = Date.now() + this.timeout;
}
}
}
const breaker = new CircuitBreaker();
async function sendEmailSafely(data) {
return breaker.execute(() => sendEmail(data));
}4. Monitor Error Rates
const errorMetrics = {
total: 0,
byCode: {},
byStatusCode: {}
};
function trackError(error) {
errorMetrics.total++;
errorMetrics.byCode[error.code] = (errorMetrics.byCode[error.code] || 0) + 1;
errorMetrics.byStatusCode[error.statusCode] = (errorMetrics.byStatusCode[error.statusCode] || 0) + 1;
// Alert if error rate is high
if (errorMetrics.total > 100 && errorMetrics.byStatusCode[500] / errorMetrics.total > 0.1) {
alertAdmin('High server error rate');
}
}Common Errors and Solutions
| Error | Cause | Solution |
|---|---|---|
ERR_AUTH_001 | Invalid API key | Check API key in dashboard |
ERR_AUTH_002 | Expired API key | Create new API key |
ERR_AUTH_003 | Insufficient permissions | Update API key permissions |
ERR_VALID_001 | Validation failed | Check request body against API docs |
ERR_TMPL_002 | Template not found | Verify template ID exists |
ERR_TMPL_004 | Missing variable | Provide all required template variables |
ERR_EMAIL_001 | Invalid email | Validate email format |
ERR_EMAIL_002 | Suppressed email | Remove from suppression list |
ERR_QUOTA_001 | Quota exceeded | Upgrade plan or wait for reset |
500 | Server error | Retry with exponential backoff |
503 | Service unavailable | Check status page, retry later |
Related: Authentication | Rate Limiting