Error Handling
Learn how to handle errors gracefully when using the Transactional API and SDKs.
Error Handling
Proper error handling ensures your application can gracefully recover from failures and provide meaningful feedback to users. This guide covers error handling for the Transactional API and SDKs.
Error Response Format
All API errors follow a consistent format:
{
"success": false,
"error": {
"code": "INVALID_API_KEY",
"message": "The API key provided is invalid.",
"statusCode": 401,
"details": {}
}
}Common Error Codes
Authentication Errors (4xx)
| Code | HTTP Status | Description |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid API key |
INVALID_API_KEY | 401 | API key format is invalid |
API_KEY_REVOKED | 401 | API key has been revoked |
FORBIDDEN | 403 | API key lacks required permissions |
Validation Errors (400)
| Code | HTTP Status | Description |
|---|---|---|
VALIDATION_ERROR | 400 | Request body validation failed |
INVALID_EMAIL | 400 | Email address format is invalid |
INVALID_PHONE | 400 | Phone number format is invalid |
MISSING_REQUIRED_FIELD | 400 | Required field is missing |
Resource Errors (404)
| Code | HTTP Status | Description |
|---|---|---|
NOT_FOUND | 404 | Resource not found |
TEMPLATE_NOT_FOUND | 404 | Email template not found |
MESSAGE_NOT_FOUND | 404 | Message ID not found |
FORM_NOT_FOUND | 404 | Form not found |
Sending Errors
| Code | HTTP Status | Description |
|---|---|---|
INACTIVE_RECIPIENT | 406 | Recipient is inactive (bounced) |
SUPPRESSED | 406 | Recipient is suppressed |
SENDER_NOT_VERIFIED | 400 | Sender email not verified |
DOMAIN_NOT_VERIFIED | 400 | Sending domain not verified |
Rate Limiting
| Code | HTTP Status | Description |
|---|---|---|
RATE_LIMITED | 429 | Too many requests |
Server Errors
| Code | HTTP Status | Description |
|---|---|---|
INTERNAL_ERROR | 500 | Server error |
SERVICE_UNAVAILABLE | 503 | Service temporarily unavailable |
SDK Error Handling
TypeScript SDK
The SDK throws typed errors you can catch and handle:
import { Transactional, ApiError, ValidationError } from 'transactional-sdk';
const client = new Transactional({
apiKey: process.env.TRANSACTIONAL_API_KEY,
});
async function sendEmail(to: string, subject: string, html: string) {
try {
const result = await client.emails.send({
from: 'hello@yourapp.com',
to,
subject,
html,
});
return { success: true, messageId: result.messageId };
} catch (error) {
if (error instanceof ApiError) {
switch (error.code) {
case 'INACTIVE_RECIPIENT':
// Handle bounced email
await markEmailAsInactive(to);
return { success: false, error: 'Email address is inactive' };
case 'SUPPRESSED':
// Handle suppressed email
return { success: false, error: 'Recipient has unsubscribed' };
case 'SENDER_NOT_VERIFIED':
// Handle unverified sender
console.error('Sender not verified:', error.message);
return { success: false, error: 'Configuration error' };
case 'RATE_LIMITED':
// Handle rate limiting
const retryAfter = error.headers?.['retry-after'] || 60;
await sleep(retryAfter * 1000);
return sendEmail(to, subject, html); // Retry
default:
console.error('API Error:', error.code, error.message);
return { success: false, error: 'Failed to send email' };
}
}
if (error instanceof ValidationError) {
console.error('Validation failed:', error.errors);
return { success: false, error: 'Invalid request data' };
}
// Network or other errors
console.error('Unexpected error:', error);
return { success: false, error: 'An unexpected error occurred' };
}
}Python SDK
from transactional import Transactional, ApiError, ValidationError
import os
client = Transactional(api_key=os.environ["TRANSACTIONAL_API_KEY"])
def send_email(to: str, subject: str, html: str):
try:
result = client.emails.send(
from_email="hello@yourapp.com",
to=to,
subject=subject,
html=html,
)
return {"success": True, "message_id": result.message_id}
except ApiError as e:
if e.code == "INACTIVE_RECIPIENT":
mark_email_as_inactive(to)
return {"success": False, "error": "Email address is inactive"}
elif e.code == "SUPPRESSED":
return {"success": False, "error": "Recipient has unsubscribed"}
elif e.code == "RATE_LIMITED":
time.sleep(60)
return send_email(to, subject, html) # Retry
else:
print(f"API Error: {e.code} - {e.message}")
return {"success": False, "error": "Failed to send email"}
except ValidationError as e:
print(f"Validation failed: {e.errors}")
return {"success": False, "error": "Invalid request data"}
except Exception as e:
print(f"Unexpected error: {e}")
return {"success": False, "error": "An unexpected error occurred"}Retry Strategies
Exponential Backoff
async function sendWithRetry(
message: EmailMessage,
maxRetries: number = 3
): Promise<SendResult> {
let lastError: Error | null = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await client.emails.send(message);
} catch (error) {
lastError = error;
if (error instanceof ApiError) {
// Don't retry these errors
if (['INACTIVE_RECIPIENT', 'SUPPRESSED', 'VALIDATION_ERROR'].includes(error.code)) {
throw error;
}
// Retry rate limits with backoff
if (error.code === 'RATE_LIMITED') {
const retryAfter = parseInt(error.headers?.['retry-after'] || '60');
await sleep(retryAfter * 1000);
continue;
}
// Retry server errors with exponential backoff
if (error.statusCode >= 500) {
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
await sleep(delay);
continue;
}
}
throw error;
}
}
throw lastError;
}Circuit Breaker
class CircuitBreaker {
private failures = 0;
private lastFailure: Date | null = null;
private state: 'closed' | 'open' | 'half-open' = 'closed';
constructor(
private threshold: number = 5,
private resetTimeout: number = 30000
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (Date.now() - this.lastFailure!.getTime() > this.resetTimeout) {
this.state = 'half-open';
} else {
throw new Error('Circuit breaker is open');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failures = 0;
this.state = 'closed';
}
private onFailure() {
this.failures++;
this.lastFailure = new Date();
if (this.failures >= this.threshold) {
this.state = 'open';
}
}
}
// Usage
const circuitBreaker = new CircuitBreaker();
async function sendEmail(message: EmailMessage) {
return circuitBreaker.execute(() => client.emails.send(message));
}Batch Error Handling
For batch operations, handle individual failures:
const results = await client.emails.sendBatch(messages);
const successful: SendResult[] = [];
const failed: { message: EmailMessage; error: SendResult }[] = [];
results.forEach((result, index) => {
if (result.errorCode === 0) {
successful.push(result);
} else {
failed.push({ message: messages[index], error: result });
}
});
console.log(`Sent: ${successful.length}, Failed: ${failed.length}`);
// Handle failures by error type
for (const { message, error } of failed) {
switch (error.errorCode) {
case 406: // INACTIVE_RECIPIENT
await handleInactiveRecipient(message.to);
break;
case 300: // Invalid email
await logInvalidEmail(message.to);
break;
default:
// Queue for retry
await retryQueue.add(message);
}
}Webhook Error Handling
Handle errors in webhook processing:
app.post('/webhooks/transactional', async (req, res) => {
try {
const event = verifyWebhookSignature(req.body, req.headers);
await processEvent(event);
res.sendStatus(200);
} catch (error) {
if (error.message === 'Invalid signature') {
console.error('Webhook signature verification failed');
res.status(401).send('Invalid signature');
return;
}
// Log but don't fail - we want to return 200 to prevent retries
// if the error is in our processing logic
console.error('Webhook processing error:', error);
// Queue for retry processing
await webhookRetryQueue.add(req.body);
res.sendStatus(200);
}
});Logging Best Practices
Structured Logging
import winston from 'winston';
const logger = winston.createLogger({
format: winston.format.json(),
transports: [new winston.transports.Console()],
});
async function sendEmail(message: EmailMessage) {
const startTime = Date.now();
try {
const result = await client.emails.send(message);
logger.info('Email sent', {
messageId: result.messageId,
to: message.to,
duration: Date.now() - startTime,
});
return result;
} catch (error) {
logger.error('Email send failed', {
to: message.to,
error: error instanceof ApiError ? error.code : 'UNKNOWN',
message: error.message,
duration: Date.now() - startTime,
});
throw error;
}
}Error Tracking
import * as Sentry from '@sentry/node';
async function sendEmail(message: EmailMessage) {
try {
return await client.emails.send(message);
} catch (error) {
// Only report unexpected errors to Sentry
if (error instanceof ApiError) {
if (!['INACTIVE_RECIPIENT', 'SUPPRESSED', 'RATE_LIMITED'].includes(error.code)) {
Sentry.captureException(error, {
extra: {
to: message.to,
errorCode: error.code,
},
});
}
} else {
Sentry.captureException(error);
}
throw error;
}
}User-Friendly Error Messages
Map API errors to user-friendly messages:
const ERROR_MESSAGES: Record<string, string> = {
INACTIVE_RECIPIENT: 'This email address appears to be inactive. Please use a different email.',
SUPPRESSED: 'This recipient has opted out of receiving emails.',
INVALID_EMAIL: 'Please enter a valid email address.',
RATE_LIMITED: 'Too many requests. Please try again in a moment.',
SENDER_NOT_VERIFIED: 'There was a configuration issue. Please contact support.',
INTERNAL_ERROR: 'An unexpected error occurred. Please try again later.',
};
function getUserFriendlyError(error: ApiError): string {
return ERROR_MESSAGES[error.code] || 'An error occurred. Please try again.';
}Next Steps
- API Reference - Complete error code reference
- Rate Limiting - Understanding rate limits
- Webhooks - Error handling for webhooks
On This Page
- Error Handling
- Error Response Format
- Common Error Codes
- Authentication Errors (4xx)
- Validation Errors (400)
- Resource Errors (404)
- Sending Errors
- Rate Limiting
- Server Errors
- SDK Error Handling
- TypeScript SDK
- Python SDK
- Retry Strategies
- Exponential Backoff
- Circuit Breaker
- Batch Error Handling
- Webhook Error Handling
- Logging Best Practices
- Structured Logging
- Error Tracking
- User-Friendly Error Messages
- Next Steps