API Error Handling

Design clear, consistent, and helpful error responses that make debugging easy and improve developer experience

Standard Error Response Format

A well-designed error response should be consistent, informative, and actionable. Every error response from your API should follow the same structure, making it easy for clients to parse and handle errors programmatically.

  • Consistent structure across all endpoints
  • Machine-readable error codes
  • Human-readable messages
  • Detailed field-level errors when applicable
  • Request tracking for debugging
400 Error Response
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input data",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format"
      },
      {
        "field": "age",
        "message": "Must be a positive number"
      }
    ],
    "request_id": "req_abc123xyz"
  }
}

Error Codes vs HTTP Status

HTTP status codes and application error codes serve different purposes. Understanding when to use each is crucial for a well-designed API. See our complete status codes reference for all HTTP codes.

🌐 HTTP Status Codes

Transport-level indicators. They tell the client about the general category of the response (success, client error, server error). Proxies, load balancers, and HTTP clients use these.

🏷️ Application Error Codes

Business logic identifiers. They provide specific, machine-readable codes that clients can use to handle errors programmatically and show appropriate UI messages.

Error Code Mapping

400 VALIDATION_ERROR, INVALID_JSON, MISSING_FIELD
401 AUTH_REQUIRED, TOKEN_EXPIRED, INVALID_TOKEN
403 FORBIDDEN, INSUFFICIENT_PERMISSIONS
404 NOT_FOUND, USER_NOT_FOUND, RESOURCE_DELETED
409 CONFLICT, DUPLICATE_EMAIL, VERSION_MISMATCH
422 UNPROCESSABLE, BUSINESS_RULE_VIOLATION
429 RATE_LIMITED, QUOTA_EXCEEDED
500 INTERNAL_ERROR, SERVICE_ERROR

Validation Errors

Return all validation errors at once. Don't make users fix one error at a time.

400

Field-Level Errors

Always include the field name and a clear message for each invalid field.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "code": "INVALID_FORMAT",
        "message": "Must be a valid email"
      }
    ]
  }
}
400

Multiple Errors

Return all errors in a single response so clients can fix everything at once.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Multiple validation errors",
    "details": [
      {"field": "email", "message": "Required"},
      {"field": "password", "message": "Min 8 chars"},
      {"field": "age", "message": "Must be 18+"}
    ]
  }
}
400

Nested Field Paths

Use dot notation or JSON pointer for nested objects and arrays.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid address data",
    "details": [
      {
        "field": "address.zip_code",
        "message": "Invalid format"
      },
      {
        "field": "items[0].quantity",
        "message": "Must be positive"
      }
    ]
  }
}

Common Error Scenarios

400 Bad Request - Invalid Input

POST /api/v1/users
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input data",
    "details": [
      {"field": "email", "message": "Invalid email format"},
      {"field": "password", "message": "Must be at least 8 characters"}
    ],
    "request_id": "req_2xK9mN3p"
  }
}

401 Unauthorized - Authentication Required

GET /api/v1/users/me
{
  "error": {
    "code": "AUTH_REQUIRED",
    "message": "Authentication is required to access this resource",
    "docs_url": "https://api.example.com/docs/authentication",
    "request_id": "req_8jH2kL5m"
  }
}

403 Forbidden - Insufficient Permissions

DELETE /api/v1/users/456
{
  "error": {
    "code": "INSUFFICIENT_PERMISSIONS",
    "message": "You don't have permission to delete this user",
    "required_role": "admin",
    "request_id": "req_9pQ4rS7t"
  }
}

404 Not Found - Resource Missing

GET /api/v1/users/99999
{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "User with ID 99999 was not found",
    "request_id": "req_3nM7kP2q"
  }
}

409 Conflict - Duplicate Resource

POST /api/v1/users
{
  "error": {
    "code": "DUPLICATE_EMAIL",
    "message": "A user with this email already exists",
    "field": "email",
    "request_id": "req_5tY8uV1w"
  }
}

422 Unprocessable Entity - Business Logic Error

POST /api/v1/orders
{
  "error": {
    "code": "INSUFFICIENT_INVENTORY",
    "message": "Cannot process order: insufficient inventory",
    "details": [
      {"product_id": 123, "requested": 10, "available": 3}
    ],
    "request_id": "req_7wX2yZ4a"
  }
}

429 Too Many Requests - Rate Limited

GET /api/v1/search
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests. Please slow down.",
    "retry_after": 60,
    "limit": 100,
    "remaining": 0,
    "reset_at": "2024-01-15T10:31:00Z",
    "request_id": "req_1aB3cD5e"
  }
}

500 Internal Server Error

POST /api/v1/process
{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "An unexpected error occurred. Please try again later.",
    "request_id": "req_6fG8hI0j",
    "support_url": "https://support.example.com"
  }
}

502 Bad Gateway

GET /api/v1/external-data
{
  "error": {
    "code": "UPSTREAM_ERROR",
    "message": "Unable to reach upstream service",
    "request_id": "req_2kL4mN6o"
  }
}

503 Service Unavailable

GET /api/v1/users
{
  "error": {
    "code": "SERVICE_UNAVAILABLE",
    "message": "Service is temporarily unavailable for maintenance",
    "retry_after": 300,
    "status_page": "https://status.example.com",
    "request_id": "req_4pQ6rS8t"
  }
}

Best Practices

🔒 Never Expose Stack Traces

✅ Good "message": "An error occurred processing your request"
❌ Bad "stack": "Error at db.js:42\n at connect..."

Stack traces expose internal implementation details and potential security vulnerabilities. Log them server-side instead.

📝 Log Errors Server-Side

✅ Server logs { "request_id": "req_abc123", "error": "...", "stack": "...", "user_id": 456 }
✅ Client response { "error": { "code": "INTERNAL_ERROR", "request_id": "req_abc123" }}

Include request_id in both logs and response so support can correlate client reports with server logs.

🎫 Provide Request IDs

✅ Good "request_id": "req_2xK9mN3pQr5t" Header: X-Request-ID: req_2xK9mN3pQr5t

Every request should have a unique ID. Return it in both the response body and headers for easy debugging.

📚 Include Documentation Links

✅ Good "docs_url": "https://api.example.com/docs/errors#AUTH_REQUIRED"

Link to relevant documentation so developers can quickly understand and resolve issues.

⏱️ Include Retry Information

✅ Good "retry_after": 60 Header: Retry-After: 60

For rate limits and temporary failures, tell clients when they can retry.

🌍 Use Consistent Error Codes

✅ Good VALIDATION_ERROR, AUTH_REQUIRED, NOT_FOUND, RATE_LIMITED
❌ Bad error_1, err-validation, Error404

Use UPPER_SNAKE_CASE for error codes. Document all possible codes in your API reference.

Error Response Schema

A complete reference schema for your error responses

JSON Error Response Schema
{
  "error": {
    // Required fields
    "code": "string",          // Machine-readable error code (UPPER_SNAKE_CASE)
    "message": "string",       // Human-readable message
    
    // Optional fields
    "request_id": "string",    // Unique request identifier for debugging
    "details": [               // Field-level errors (for validation)
      {
        "field": "string",     // Field name (dot notation for nested)
        "code": "string",      // Field-specific error code
        "message": "string"    // Field-specific message
      }
    ],
    "docs_url": "string",      // Link to error documentation
    "retry_after": "number",   // Seconds until client can retry
    "support_url": "string"    // Link to support/status page
  }
}