Skip to main content

Error Handling

The Fanfare API uses conventional HTTP response codes to indicate the success or failure of an API request.

Error Response Format

All error responses follow a consistent JSON structure:
{
  "error": "Error type or message",
  "message": "Human-readable description (optional)",
  "details": [
    {
      "field": "fieldName",
      "message": "Field-specific error message"
    }
  ]
}

HTTP Status Codes

Success Codes (2xx)

CodeDescription
200 OKThe request succeeded
201 CreatedA new resource was created
204 No ContentThe request succeeded with no response body

Client Error Codes (4xx)

CodeDescription
400 Bad RequestThe request was malformed or contained invalid data
401 UnauthorizedAuthentication credentials were missing or invalid
403 ForbiddenThe authenticated user lacks permission for the requested action
404 Not FoundThe requested resource does not exist
409 ConflictThe request conflicts with the current state of the resource
413 Payload Too LargeThe request body exceeds the maximum allowed size
423 LockedThe resource is temporarily unavailable (e.g., queue not yet open)
429 Too Many RequestsRate limit exceeded

Server Error Codes (5xx)

CodeDescription
500 Internal Server ErrorAn unexpected error occurred on the server
503 Service UnavailableThe service is temporarily unavailable

Common Error Types

Authentication Errors

Missing Authentication
HTTP/1.1 401 Unauthorized
{
  "error": "Authentication required"
}
Invalid Token
HTTP/1.1 401 Unauthorized
{
  "error": "invalid refresh token"
}
Secret Key Required
HTTP/1.1 401 Unauthorized
{
  "error": "Secret key required"
}

Validation Errors

Field Validation Failed
HTTP/1.1 400 Bad Request
{
  "error": "Validation failed",
  "details": [
    {
      "field": "email",
      "message": "Invalid email format"
    },
    {
      "field": "capacity",
      "message": "Must be a positive integer"
    }
  ]
}
Missing Required Field
HTTP/1.1 400 Bad Request
{
  "error": "Validation failed",
  "details": [
    {
      "field": "name",
      "message": "Required"
    }
  ]
}
Database Constraint Violation
HTTP/1.1 400 Bad Request
{
  "error": "Validation failed",
  "details": [
    {
      "field": "slug",
      "message": "Already taken"
    }
  ]
}

Resource Errors

Resource Not Found
HTTP/1.1 404 Not Found
{
  "error": "Experience not found"
}
Resource Conflict
HTTP/1.1 409 Conflict
{
  "error": "Sync already in progress"
}

State Errors

Queue Not Open
HTTP/1.1 423 Locked
Retry-After: 2024-12-01T09:00:00Z
{
  "error": "Queue is not open yet"
}
Queue Full
HTTP/1.1 429 Too Many Requests
Retry-After: 30
{
  "error": "Queue is full"
}
Auction Closed
HTTP/1.1 400 Bad Request
{
  "error": "Auction is closed"
}

OTP Errors

OTP Expired
HTTP/1.1 401 Unauthorized
{
  "error": "otp_expired",
  "message": "Your verification code has expired. Please request a new one."
}
Invalid OTP
HTTP/1.1 401 Unauthorized
{
  "error": "otp_invalid",
  "message": "Invalid verification code. Please try again."
}

Fingerprint Errors

For bot mitigation, some endpoints validate device fingerprints: Missing Fingerprint
HTTP/1.1 400 Bad Request
X-Fingerprint-Error: true
{
  "error": "FINGERPRINT_REQUIRED",
  "code": "FP001",
  "message": "Device fingerprint is required for this action",
  "support": {
    "message": "If you need assistance, please contact support with this reference code.",
    "referenceCode": "ABC123XYZ"
  }
}
Device Mismatch
HTTP/1.1 403 Forbidden
X-Fingerprint-Mismatch: true
{
  "valid": false,
  "error": "DEVICE_MISMATCH",
  "message": "This access token can only be used on the device that originally entered the queue.",
  "code": "FP002",
  "support": {
    "message": "If you need to switch devices, please contact support with your confirmation code.",
    "confirmationCode": "DEF456UVW"
  }
}

Rate Limiting Errors

HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1701424860
{
  "error": "Rate limit exceeded",
  "retryAfter": 60
}

Order Limit Errors

When a consumer exceeds purchase limits:
{
  "status": "DENIED",
  "denyReason": "ORDER_LIMIT_EXCEEDED",
  "consumerId": "cons_01HXYZ123456789"
}

Error Handling Best Practices

1. Check HTTP Status Code First

const response = await fetch(url, options);

if (!response.ok) {
  const error = await response.json();
  // Handle based on status code
  switch (response.status) {
    case 400:
      handleValidationError(error);
      break;
    case 401:
      handleAuthError(error);
      break;
    case 429:
      handleRateLimitError(error, response.headers);
      break;
    default:
      handleGenericError(error);
  }
}

2. Respect Retry-After Headers

if (response.status === 429 || response.status === 423) {
  const retryAfter = response.headers.get("Retry-After");
  if (retryAfter) {
    // Could be seconds or ISO timestamp
    const delay = isNaN(retryAfter) ? new Date(retryAfter).getTime() - Date.now() : parseInt(retryAfter) * 1000;
    await sleep(delay);
    // Retry the request
  }
}

3. Display User-Friendly Messages

Map error codes to user-friendly messages:
const errorMessages = {
  otp_expired: "Your code has expired. Please request a new one.",
  otp_invalid: "That code doesn't look right. Please check and try again.",
  DEVICE_MISMATCH: "Please use the same device you used to join the queue.",
  "Queue is full": "Sorry, this queue is currently full. Please try again later.",
};

4. Log Errors for Debugging

Include relevant context in error logs:
console.error("API Error", {
  status: response.status,
  error: error.error,
  details: error.details,
  requestId: response.headers.get("X-Request-Id"),
  timestamp: new Date().toISOString(),
});

Error Recovery Strategies

Transient Errors (5xx)

Implement exponential backoff:
async function retryWithBackoff(fn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      if (error.status >= 500) {
        await sleep(Math.pow(2, i) * 1000);
      } else {
        throw error; // Don't retry client errors
      }
    }
  }
}

Token Expiration

Automatically refresh tokens when expired:
async function apiCall(url, options) {
  const response = await fetch(url, options);

  if (response.status === 401) {
    const refreshed = await refreshToken();
    if (refreshed) {
      // Retry with new token
      return fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          Authorization: `Bearer ${refreshed.accessToken}`,
        },
      });
    }
  }

  return response;
}