REST 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. This structure lets clients 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. You must know when to use each one 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. Do not 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
  }
}