Developer GuideError Handling

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

FieldTypeDescription
statusCodenumberHTTP status code
codestringError code (e.g., ERR_VALID_001)
messagestringHuman-readable error message
timestampstringISO 8601 timestamp
pathstringAPI endpoint that generated error
relatedInfoobjectAdditional context (optional)

HTTP Status Codes

Success Codes (2xx)

CodeDescription
200 OKRequest successful
201 CreatedResource created successfully
204 No ContentRequest successful, no content to return

Client Error Codes (4xx)

CodeDescriptionWhen to Expect
400 Bad RequestInvalid request dataValidation errors, malformed JSON
401 UnauthorizedAuthentication requiredMissing or invalid API key
403 ForbiddenInsufficient permissionsAPI key lacks required permission
404 Not FoundResource doesn’t existTemplate, email, or endpoint not found
409 ConflictResource conflictDuplicate resource, version mismatch
422 Unprocessable EntityValid syntax but semantic errorsBusiness logic violation
429 Too Many RequestsRate limit exceededToo many requests in time window

Server Error Codes (5xx)

CodeDescriptionWhat to Do
500 Internal Server ErrorUnexpected server errorRetry with exponential backoff
502 Bad GatewayUpstream service errorRetry after delay
503 Service UnavailableService temporarily downCheck status page, retry later
504 Gateway TimeoutRequest timeoutRetry with longer timeout

Error Codes

Authentication Errors (ERR_AUTH_xxx)

{
  "statusCode": 401,
  "code": "ERR_AUTH_001",
  "message": "Invalid API key"
}
CodeMessageSolution
ERR_AUTH_001Invalid API keyCheck API key is correct
ERR_AUTH_002API key has expiredCreate new API key
ERR_AUTH_003Insufficient permissionsCheck 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" }
    ]
  }
}
CodeMessage
ERR_VALID_001Validation failed
ERR_VALID_002Invalid email format
ERR_VALID_003Required field missing

Template Errors (ERR_TMPL_xxx)

{
  "statusCode": 404,
  "code": "ERR_TMPL_002",
  "message": "Email template not found",
  "relatedInfo": {
    "templateId": "tmpl_invalid"
  }
}
CodeMessage
ERR_TMPL_001Template validation failed
ERR_TMPL_002Template not found
ERR_TMPL_003Template is not published
ERR_TMPL_004Missing required variable

Email Errors (ERR_EMAIL_xxx)

{
  "statusCode": 400,
  "code": "ERR_EMAIL_001",
  "message": "Invalid recipient email address",
  "relatedInfo": {
    "email": "invalid-email"
  }
}
CodeMessage
ERR_EMAIL_001Invalid recipient email
ERR_EMAIL_002Recipient on suppression list
ERR_EMAIL_003Too 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"
  }
}
CodeMessage
ERR_QUOTA_001Monthly quota exceeded
ERR_QUOTA_002Daily 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 Error
  • 502 Bad Gateway
  • 503 Service Unavailable
  • 504 Gateway Timeout
  • 429 Too Many Requests (with backoff from Retry-After header)
  • Network timeouts
  • Connection errors

Don’t retry these:

  • 400 Bad Request - Fix request data
  • 401 Unauthorized - Fix authentication
  • 403 Forbidden - Fix permissions
  • 404 Not Found - Resource doesn’t exist
  • 409 Conflict - Fix conflict
  • 422 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: 60

Using 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

ErrorCauseSolution
ERR_AUTH_001Invalid API keyCheck API key in dashboard
ERR_AUTH_002Expired API keyCreate new API key
ERR_AUTH_003Insufficient permissionsUpdate API key permissions
ERR_VALID_001Validation failedCheck request body against API docs
ERR_TMPL_002Template not foundVerify template ID exists
ERR_TMPL_004Missing variableProvide all required template variables
ERR_EMAIL_001Invalid emailValidate email format
ERR_EMAIL_002Suppressed emailRemove from suppression list
ERR_QUOTA_001Quota exceededUpgrade plan or wait for reset
500Server errorRetry with exponential backoff
503Service unavailableCheck status page, retry later

Related: Authentication | Rate Limiting