Skip to main content

API Error Handling Implementation Guide

Table of Contents

  1. Overview
  2. Error Response Format
  3. Error Types
  4. Error Code Taxonomy
  5. Client Retry Logic
  6. Error Logging
  7. User-Friendly Messages
  8. Implementation Examples

Overview

This guide establishes standardized error handling practices for all APIs in the vacation rental platform. It is based on RFC 7807: Problem Details for HTTP APIs, which provides a machine-readable format for specifying errors in HTTP API responses.

Goals

  • Consistency: Uniform error format across all microservices
  • Debuggability: Rich error context for developers
  • User Experience: Clear, actionable error messages for end users
  • Automation: Machine-readable errors for client-side error handling
  • Observability: Comprehensive error logging and tracking

Key Principles

  1. Use standard HTTP status codes correctly
  2. Return RFC 7807-compliant problem details
  3. Include correlation IDs for tracing
  4. Separate technical details from user-facing messages
  5. Never expose sensitive data in error responses
  6. Log errors with appropriate severity levels

RFC 7807 Benefits

  • Standardization: Industry-standard format (application/problem+json)
  • Extensibility: Custom fields for domain-specific context
  • Tool Support: Wide ecosystem support for parsing and handling
  • Documentation: Self-documenting error types via URI references

Error Response Format

Base Problem Details Structure

All error responses must follow this RFC 7807 format:

{
"type": "https://api.tvl.com/errors/validation-error",
"title": "Validation Error",
"status": 400,
"detail": "Unit name is required and must be between 1-255 characters",
"instance": "/api/v1/units/123",
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2025-10-25T14:30:00Z"
}

Field Definitions

FieldRequiredTypeDescription
typeYesURIURI reference identifying the problem type. Must be dereferenceable documentation
titleYesStringShort, human-readable summary. Should not change from occurrence to occurrence
statusYesIntegerHTTP status code (duplicated for convenience)
detailYesStringHuman-readable explanation specific to this occurrence
instanceYesURIURI reference identifying the specific occurrence. Should include resource path
traceIdYesUUIDCorrelation ID for distributed tracing (OpenTelemetry trace ID)
timestampYesISO8601When the error occurred

Extended Fields for Validation Errors

For validation errors (400), include detailed field-level errors:

{
"type": "https://api.tvl.com/errors/validation-error",
"title": "Validation Error",
"status": 400,
"detail": "Request validation failed on 3 fields",
"instance": "/api/v1/units",
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2025-10-25T14:30:00Z",
"errors": [
{
"field": "name",
"code": "REQUIRED",
"message": "Unit name is required",
"rejectedValue": null
},
{
"field": "maxOccupancy",
"code": "MIN_VALUE",
"message": "Max occupancy must be at least 1",
"rejectedValue": 0
},
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Email must be a valid email address",
"rejectedValue": "not-an-email"
}
]
}

Extended Fields for Business Logic Errors

For domain-specific errors (409, 422), include business context:

{
"type": "https://api.tvl.com/errors/booking-conflict",
"title": "Booking Conflict",
"status": 409,
"detail": "Unit is already booked for the requested dates",
"instance": "/api/v1/bookings",
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2025-10-25T14:30:00Z",
"errorCode": "BOOKING_CONFLICT",
"conflictingBookingId": "bkg_789",
"requestedCheckIn": "2025-11-01",
"requestedCheckOut": "2025-11-05",
"availableFrom": "2025-11-06"
}

Content-Type Header

Always return error responses with:

Content-Type: application/problem+json; charset=utf-8

Error Types

1. Validation Errors (400 Bad Request)

When to use: Client sent syntactically or semantically invalid request data.

{
"type": "https://api.tvl.com/errors/validation-error",
"title": "Validation Error",
"status": 400,
"detail": "Request validation failed",
"errorCode": "VALIDATION_ERROR"
}

Common scenarios:

  • Missing required fields
  • Invalid data types or formats
  • Values outside allowed ranges
  • Invalid enum values
  • Malformed JSON

Retryable: No (fix request and retry)

2. Authentication Errors (401 Unauthorized)

When to use: Authentication is required but missing or invalid.

{
"type": "https://api.tvl.com/errors/authentication-required",
"title": "Authentication Required",
"status": 401,
"detail": "Valid authentication credentials are required to access this resource",
"errorCode": "AUTH_TOKEN_MISSING",
"wwwAuthenticate": "Bearer realm=\"TVL API\""
}

Common scenarios:

  • Missing Authorization header
  • Expired JWT token
  • Invalid token signature
  • Revoked credentials

Retryable: No (obtain valid credentials first)

Variants:

  • AUTH_TOKEN_MISSING: No token provided
  • AUTH_TOKEN_EXPIRED: Token expired
  • AUTH_TOKEN_INVALID: Token signature invalid
  • AUTH_TOKEN_REVOKED: Token has been revoked

3. Authorization Errors (403 Forbidden)

When to use: Authenticated user lacks permission for the requested operation.

{
"type": "https://api.tvl.com/errors/authorization-error",
"title": "Forbidden",
"status": 403,
"detail": "You do not have permission to delete this unit",
"errorCode": "INSUFFICIENT_PERMISSIONS",
"requiredPermission": "units:delete",
"resourceId": "unit_123"
}

Common scenarios:

  • Insufficient role/permissions
  • Cross-tenant access attempt
  • Resource ownership violation
  • Policy-based access denial

Retryable: No (requires permission change)

4. Not Found Errors (404 Not Found)

When to use: Requested resource does not exist.

{
"type": "https://api.tvl.com/errors/resource-not-found",
"title": "Resource Not Found",
"status": 404,
"detail": "Unit with ID 'unit_123' was not found",
"errorCode": "RESOURCE_NOT_FOUND",
"resourceType": "Unit",
"resourceId": "unit_123"
}

Common scenarios:

  • Invalid resource ID
  • Deleted resource
  • Typo in URL path
  • Resource not visible due to tenant isolation

Retryable: No (unless resource is being created asynchronously)

5. Conflict Errors (409 Conflict)

When to use: Request conflicts with current state of the resource.

{
"type": "https://api.tvl.com/errors/booking-conflict",
"title": "Booking Conflict",
"status": 409,
"detail": "Unit is already booked for the requested dates",
"errorCode": "BOOKING_DATE_CONFLICT",
"conflictingBookingId": "bkg_789",
"requestedCheckIn": "2025-11-01",
"requestedCheckOut": "2025-11-05"
}

Common scenarios:

  • Double booking attempts
  • Duplicate resource creation
  • Optimistic locking failures
  • State transition violations

Retryable: Sometimes (after resolving conflict)

6. Unprocessable Entity (422 Unprocessable Entity)

When to use: Request is well-formed but semantically invalid.

{
"type": "https://api.tvl.com/errors/business-rule-violation",
"title": "Business Rule Violation",
"status": 422,
"detail": "Check-out date must be after check-in date",
"errorCode": "INVALID_DATE_RANGE",
"checkInDate": "2025-11-05",
"checkOutDate": "2025-11-03"
}

Common scenarios:

  • Business rule violations
  • Invalid state transitions
  • Logical inconsistencies
  • Cross-field validation failures

Retryable: No (fix business logic and retry)

7. Rate Limit Errors (429 Too Many Requests)

When to use: Client has exceeded rate limits.

{
"type": "https://api.tvl.com/errors/rate-limit-exceeded",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "You have exceeded the rate limit of 100 requests per minute",
"errorCode": "RATE_LIMIT_EXCEEDED",
"limit": 100,
"remaining": 0,
"resetAt": "2025-10-25T14:31:00Z"
}

Headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1730302260
Retry-After: 60

Retryable: Yes (after reset time)

8. Server Errors (500 Internal Server Error)

When to use: Unexpected server-side error occurred.

{
"type": "https://api.tvl.com/errors/internal-server-error",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred. Please try again later.",
"errorCode": "INTERNAL_SERVER_ERROR",
"traceId": "550e8400-e29b-41d4-a716-446655440000"
}

Important: Never expose stack traces, database errors, or internal details to clients.

Retryable: Yes (with exponential backoff)

9. Service Unavailable (503 Service Unavailable)

When to use: Service is temporarily unavailable.

{
"type": "https://api.tvl.com/errors/service-unavailable",
"title": "Service Unavailable",
"status": 503,
"detail": "Service is temporarily unavailable. Please try again later.",
"errorCode": "SERVICE_UNAVAILABLE",
"retryAfter": 120
}

Headers:

Retry-After: 120

Common scenarios:

  • Deployment in progress
  • Database maintenance
  • Circuit breaker open
  • Dependency unavailable

Retryable: Yes (after retry-after period)


Error Code Taxonomy

Error Code Structure

Error codes follow a hierarchical naming convention:

{DOMAIN}_{ENTITY}_{ERROR_TYPE}

Examples:

  • BOOKING_DATE_CONFLICT
  • PRICING_CALCULATION_FAILED
  • PAYMENT_AUTHORIZATION_DECLINED
  • UNIT_AVAILABILITY_UNAVAILABLE

Domain-Specific Error Codes

Identity & Tenancy Domain

CodeStatusDescription
IDENTITY_USER_NOT_FOUND404User does not exist
IDENTITY_EMAIL_ALREADY_EXISTS409Email already registered
IDENTITY_TENANT_NOT_FOUND404Tenant does not exist
IDENTITY_TENANT_SUSPENDED403Tenant account suspended
IDENTITY_PASSWORD_TOO_WEAK422Password does not meet requirements

Authorization Domain

CodeStatusDescription
AUTH_INSUFFICIENT_PERMISSIONS403Missing required permission
AUTH_ROLE_NOT_FOUND404Role does not exist
AUTH_POLICY_VIOLATION403Access policy denied request
AUTH_CROSS_TENANT_ACCESS403Cross-tenant access not allowed

Supply Domain

CodeStatusDescription
SUPPLY_UNIT_NOT_FOUND404Unit does not exist
SUPPLY_SPACE_NOT_FOUND404Space does not exist
SUPPLY_UNIT_INACTIVE422Unit is not active
SUPPLY_MAX_OCCUPANCY_INVALID400Invalid occupancy value

Availability Domain

CodeStatusDescription
AVAILABILITY_DATE_UNAVAILABLE409Dates not available
AVAILABILITY_BLOCKED409Dates are blocked
AVAILABILITY_MIN_STAY_VIOLATION422Minimum stay requirement not met
AVAILABILITY_MAX_STAY_VIOLATION422Maximum stay exceeded

Pricing Domain

CodeStatusDescription
PRICING_CALCULATION_FAILED500Pricing calculation error
PRICING_RULE_NOT_FOUND404Pricing rule does not exist
PRICING_NO_BASE_RATE422No base rate configured
PRICING_DISCOUNT_INVALID400Invalid discount code

Booking Domain

CodeStatusDescription
BOOKING_NOT_FOUND404Booking does not exist
BOOKING_DATE_CONFLICT409Dates conflict with existing booking
BOOKING_QUOTE_EXPIRED422Quote has expired
BOOKING_ALREADY_CONFIRMED409Booking already confirmed
BOOKING_CANCELLATION_NOT_ALLOWED422Cancellation policy violation

Payment Domain

CodeStatusDescription
PAYMENT_AUTHORIZATION_FAILED422Payment authorization failed
PAYMENT_CARD_DECLINED422Card declined by issuer
PAYMENT_INSUFFICIENT_FUNDS422Insufficient funds
PAYMENT_REFUND_FAILED500Refund processing failed
PAYMENT_PROVIDER_UNAVAILABLE503Payment provider unavailable

Error Code Registry

Maintain a centralized error code registry in code:

// src/common/errors/error-codes.ts
export const ErrorCodes = {
// Validation
VALIDATION_ERROR: 'VALIDATION_ERROR',

// Identity & Tenancy
IDENTITY_USER_NOT_FOUND: 'IDENTITY_USER_NOT_FOUND',
IDENTITY_EMAIL_ALREADY_EXISTS: 'IDENTITY_EMAIL_ALREADY_EXISTS',

// Booking
BOOKING_DATE_CONFLICT: 'BOOKING_DATE_CONFLICT',
BOOKING_QUOTE_EXPIRED: 'BOOKING_QUOTE_EXPIRED',

// Add all error codes here
} as const;

export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes];

Client Retry Logic

Retryability Classification

Status CodeRetryableStrategy
400NoFix request
401NoRe-authenticate
403NoCheck permissions
404NoVerify resource exists
409ConditionalResolve conflict first
422NoFix business logic
429YesRespect Retry-After
500YesExponential backoff
502YesExponential backoff
503YesRespect Retry-After
504YesExponential backoff

Exponential Backoff Strategy

Implement exponential backoff with jitter for retryable errors:

async function retryWithBackoff<T>(
fn: () => Promise<T>,
options: {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
}
): Promise<T> {
const { maxRetries, baseDelayMs, maxDelayMs } = options;

for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries || !isRetryable(error)) {
throw error;
}

// Calculate delay with exponential backoff and jitter
const exponentialDelay = baseDelayMs * Math.pow(2, attempt);
const jitter = Math.random() * exponentialDelay * 0.1;
const delay = Math.min(exponentialDelay + jitter, maxDelayMs);

await sleep(delay);
}
}
}

function isRetryable(error: ApiError): boolean {
const retryableStatusCodes = [429, 500, 502, 503, 504];
return retryableStatusCodes.includes(error.status);
}

Rate Limit Handling

Respect Retry-After header for 429 responses:

async function handleRateLimit(error: ApiError): Promise<void> {
if (error.status === 429) {
const retryAfter = error.headers['retry-after'];

if (retryAfter) {
// Retry-After can be seconds or HTTP date
const delayMs = isNaN(Number(retryAfter))
? new Date(retryAfter).getTime() - Date.now()
: Number(retryAfter) * 1000;

await sleep(delayMs);
} else {
// Fallback to exponential backoff
await sleep(60000); // 60 seconds
}
}
}

Circuit Breaker Pattern

Implement circuit breaker to prevent cascading failures:

class CircuitBreaker {
private failureCount = 0;
private lastFailureTime = 0;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';

constructor(
private failureThreshold: number = 5,
private resetTimeoutMs: number = 60000
) {}

async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.resetTimeoutMs) {
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(): void {
this.failureCount = 0;
this.state = 'CLOSED';
}

private onFailure(): void {
this.failureCount++;
this.lastFailureTime = Date.now();

if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
}
}
}

Error Logging

What to Log

Always log:

  • Trace ID (correlation ID)
  • Timestamp
  • HTTP method and path
  • Status code
  • Error code
  • User ID (if authenticated)
  • Tenant ID (if multi-tenant)
  • Request ID

Conditionally log:

  • Request body (exclude PII)
  • Response body (sanitize)
  • Stack trace (5xx errors only)
  • Database query (if applicable)

Never log:

  • Passwords or credentials
  • Full credit card numbers
  • Social security numbers
  • Personal health information
  • API keys or tokens

Log Levels

Error TypeLog LevelAlert
400-level (client errors)INFO/WARNNo
401, 403 (auth errors)WARNAfter threshold
404 (not found)INFONo
429 (rate limit)WARNAfter threshold
500 (server error)ERRORYes
503 (unavailable)WARNAfter threshold

Structured Logging Format

Use structured JSON logging for machine readability:

{
"timestamp": "2025-10-25T14:30:00Z",
"level": "ERROR",
"message": "Internal server error occurred",
"traceId": "550e8400-e29b-41d4-a716-446655440000",
"spanId": "7f9c8d6e",
"service": "booking-service",
"environment": "production",
"error": {
"type": "https://api.tvl.com/errors/internal-server-error",
"code": "INTERNAL_SERVER_ERROR",
"status": 500,
"message": "Database connection timeout"
},
"request": {
"method": "POST",
"path": "/api/v1/bookings",
"userId": "usr_123",
"tenantId": "tnt_456",
"ip": "192.168.1.1",
"userAgent": "Mozilla/5.0..."
},
"context": {
"bookingId": "bkg_789",
"unitId": "unit_123"
},
"stack": "Error: Database connection timeout\n at ..."
}

PII Considerations

Implement PII scrubbing before logging:

const PII_FIELDS = ['password', 'ssn', 'creditCard', 'token'];

function sanitizeForLogging(obj: any): any {
if (typeof obj !== 'object' || obj === null) {
return obj;
}

const sanitized = { ...obj };

for (const key in sanitized) {
if (PII_FIELDS.some(field => key.toLowerCase().includes(field))) {
sanitized[key] = '[REDACTED]';
} else if (typeof sanitized[key] === 'object') {
sanitized[key] = sanitizeForLogging(sanitized[key]);
}
}

return sanitized;
}

Error Tracking Integration

Integrate with error tracking services (e.g., Sentry, Rollbar):

import * as Sentry from '@sentry/node';

function reportError(error: Error, context: any): void {
Sentry.captureException(error, {
level: 'error',
tags: {
service: 'booking-service',
errorCode: context.errorCode
},
extra: sanitizeForLogging(context)
});
}

User-Friendly Messages

Technical vs. User Messages

Separate technical details from user-facing messages:

{
"type": "https://api.tvl.com/errors/booking-conflict",
"title": "Booking Conflict",
"status": 409,
"detail": "Unit is already booked for the requested dates",
"userMessage": "This property is unavailable for your selected dates. Please choose different dates or view similar properties.",
"technicalDetails": {
"conflictingBookingId": "bkg_789",
"databaseQuery": "SELECT * FROM bookings WHERE ..."
}
}

Localization

Support multiple languages via Accept-Language header:

{
"type": "https://api.tvl.com/errors/validation-error",
"title": "Erreur de validation",
"status": 400,
"detail": "Le nom de l'unité est requis",
"locale": "fr-FR",
"errors": [
{
"field": "name",
"code": "REQUIRED",
"message": "Le nom de l'unité est requis"
}
]
}

Localization implementation:

const ERROR_MESSAGES = {
'en-US': {
BOOKING_DATE_CONFLICT: 'This property is unavailable for your selected dates',
VALIDATION_ERROR: 'Please check your input and try again'
},
'fr-FR': {
BOOKING_DATE_CONFLICT: 'Cette propriété n\'est pas disponible pour vos dates sélectionnées',
VALIDATION_ERROR: 'Veuillez vérifier votre saisie et réessayer'
}
};

function getLocalizedMessage(
errorCode: string,
locale: string = 'en-US'
): string {
return ERROR_MESSAGES[locale]?.[errorCode] || ERROR_MESSAGES['en-US'][errorCode];
}

Message Guidelines

Do:

  • Use simple, clear language
  • Explain what went wrong
  • Suggest how to fix it
  • Be empathetic and helpful

Don't:

  • Blame the user
  • Use technical jargon
  • Expose internal system details
  • Be vague or unhelpful

Examples:

BadGood
"Invalid input""Please enter a valid email address"
"Error 500""Something went wrong. Please try again later"
"FK constraint violation""This property cannot be deleted because it has active bookings"
"Unauthorized""You don't have permission to perform this action. Contact your administrator for access"

Implementation Examples

Express.js Middleware

import { Request, Response, NextFunction } from 'express';

interface ProblemDetails {
type: string;
title: string;
status: number;
detail: string;
instance: string;
traceId: string;
timestamp: string;
[key: string]: any;
}

class ApiError extends Error {
constructor(
public status: number,
public errorCode: string,
public detail: string,
public extensions?: Record<string, any>
) {
super(detail);
this.name = 'ApiError';
}
}

function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
): void {
const traceId = req.headers['x-trace-id'] as string || generateTraceId();

if (err instanceof ApiError) {
const problem: ProblemDetails = {
type: `https://api.tvl.com/errors/${err.errorCode.toLowerCase().replace(/_/g, '-')}`,
title: err.errorCode.replace(/_/g, ' '),
status: err.status,
detail: err.detail,
instance: req.path,
traceId,
timestamp: new Date().toISOString(),
...err.extensions
};

logError(problem, req);

res
.status(err.status)
.type('application/problem+json')
.json(problem);
} else {
// Unexpected error
const problem: ProblemDetails = {
type: 'https://api.tvl.com/errors/internal-server-error',
title: 'Internal Server Error',
status: 500,
detail: 'An unexpected error occurred',
instance: req.path,
traceId,
timestamp: new Date().toISOString()
};

logError({ ...problem, stack: err.stack }, req);

res
.status(500)
.type('application/problem+json')
.json(problem);
}
}

FastAPI (Python) Exception Handler

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from datetime import datetime
import uuid

class ApiError(Exception):
def __init__(
self,
status: int,
error_code: str,
detail: str,
extensions: dict = None
):
self.status = status
self.error_code = error_code
self.detail = detail
self.extensions = extensions or {}

@app.exception_handler(ApiError)
async def api_error_handler(request: Request, exc: ApiError):
trace_id = request.headers.get('x-trace-id', str(uuid.uuid4()))

problem = {
'type': f'https://api.tvl.com/errors/{exc.error_code.lower().replace("_", "-")}',
'title': exc.error_code.replace('_', ' ').title(),
'status': exc.status,
'detail': exc.detail,
'instance': request.url.path,
'traceId': trace_id,
'timestamp': datetime.utcnow().isoformat() + 'Z',
**exc.extensions
}

log_error(problem, request)

return JSONResponse(
status_code=exc.status,
content=problem,
media_type='application/problem+json'
)

Summary

This API error handling guide establishes a consistent, RFC 7807-compliant approach to error responses across all services. By following these standards, we ensure:

  • Developer Experience: Clear, actionable error messages with rich context
  • Client Resilience: Proper retry logic and circuit breaker patterns
  • Observability: Comprehensive error logging with PII protection
  • User Experience: Localized, user-friendly error messages

All services must implement these error handling patterns to maintain consistency and reliability across the platform.