REST API Error Handling

Errors are inevitable in any API. A client sends a bad request. A resource does not exist. The server crashes. What separates a great API from a frustrating one is how it handles these errors. A clear, consistent error response helps developers understand what went wrong and fix it quickly.

What Makes a Good Error Response?

An error response must answer three questions for the client:

  1. What went wrong?
  2. Why did it go wrong?
  3. How can the developer fix it?
Bad error response:
  HTTP 400
  { "error": "Bad Request" }
  → Tells the client nothing useful.

Good error response:
  HTTP 400 Bad Request
  {
    "status": 400,
    "error": "VALIDATION_FAILED",
    "message": "The request body is missing required fields.",
    "details": [
      { "field": "email", "issue": "Email is required." },
      { "field": "price", "issue": "Price must be a positive number." }
    ],
    "requestId": "req-7f2a8b-c3d4"
  }
  → Developer knows exactly what to fix.

Standard Error Response Structure

{
  "status": 404,
  "error": "RESOURCE_NOT_FOUND",
  "message": "The product with ID 99 was not found.",
  "path": "/v1/products/99",
  "timestamp": "2024-06-01T10:30:00Z",
  "requestId": "req-a1b2c3"
}

Field breakdown:
  status      → mirrors the HTTP status code
  error       → a machine-readable error code (uppercase, underscored)
  message     → a human-readable explanation
  path        → the URL that caused the error
  timestamp   → when the error occurred
  requestId   → a unique ID for tracking this request in server logs

Machine-Readable Error Codes

The error field should contain a short, uppercase code that a client application can check programmatically — not just the status code number.

HTTP Status  │  Error Code              │  Meaning
─────────────┼──────────────────────────┼──────────────────────────────
400          │  VALIDATION_FAILED       │  Request data has errors
400          │  MISSING_REQUIRED_FIELD  │  Required field not sent
401          │  INVALID_TOKEN           │  Token expired or invalid
401          │  TOKEN_MISSING           │  No auth token sent
403          │  INSUFFICIENT_PERMISSIONS│  User cannot do this action
404          │  RESOURCE_NOT_FOUND      │  Item does not exist
409          │  DUPLICATE_ENTRY         │  Record already exists
422          │  BUSINESS_RULE_VIOLATED  │  Logic rule failed
429          │  RATE_LIMIT_EXCEEDED     │  Too many requests
500          │  INTERNAL_ERROR          │  Unexpected server failure

A client application can do this:

if (response.error === "RATE_LIMIT_EXCEEDED") {
  waitAndRetry();
} else if (response.error === "INVALID_TOKEN") {
  redirectToLogin();
}

This is only possible with machine-readable codes, not just a message string.

Validation Errors: Show Every Problem at Once

When a request body has multiple validation errors, return all of them in one response. Do not make the developer fix one error, resubmit, discover another error, and repeat.

POST /users
{
  "username": "",
  "email": "not-an-email",
  "age": -5
}

Response: HTTP 400 Bad Request
{
  "status": 400,
  "error": "VALIDATION_FAILED",
  "message": "The request body contains validation errors.",
  "details": [
    { "field": "username", "issue": "Username cannot be empty." },
    { "field": "email",    "issue": "Email must be a valid email address." },
    { "field": "age",      "issue": "Age must be a positive number." }
  ]
}

Don't Expose Internal Details in Errors

Error messages are developer-facing, but they are also client-facing. Never include stack traces, database error messages, or internal system details in API error responses. They expose security vulnerabilities and confuse end users.

❌ Never return this:
  {
    "error": "Error: SELECT * FROM users WHERE id=99: ERROR: column \"usr_id\" does not exist LINE 1: SELECT * FROM users WHERE id=99"
  }

✅ Return this instead:
  {
    "status": 500,
    "error": "INTERNAL_ERROR",
    "message": "An unexpected error occurred. Please try again later.",
    "requestId": "req-a1b2c3"
  }

Log the full technical details server-side using the requestId.
Never send them to the client.

Error Response for Different Scenarios

Scenario 1: Resource not found
  HTTP 404
  {
    "status": 404,
    "error": "RESOURCE_NOT_FOUND",
    "message": "Order #788 does not exist or has been deleted."
  }

Scenario 2: Unauthorized access
  HTTP 401
  {
    "status": 401,
    "error": "TOKEN_EXPIRED",
    "message": "Your session has expired. Please log in again."
  }

Scenario 3: Business rule violation
  HTTP 422
  {
    "status": 422,
    "error": "BUSINESS_RULE_VIOLATED",
    "message": "Cannot cancel an order that has already been shipped.",
    "details": [
      { "field": "orderStatus", "issue": "Order is in 'shipped' state." }
    ]
  }

Scenario 4: Duplicate creation attempt
  HTTP 409
  {
    "status": 409,
    "error": "DUPLICATE_ENTRY",
    "message": "A user with this email already exists."
  }

Using a Link to Documentation

For complex errors, include a link to the error's documentation page so developers can read a full explanation.

{
  "status": 400,
  "error": "VALIDATION_FAILED",
  "message": "The price field must be greater than zero.",
  "docsUrl": "https://api.shop.com/docs/errors/VALIDATION_FAILED"
}

Key Points

  • Always return the correct HTTP status code alongside the error body
  • Include a machine-readable error code for programmatic handling
  • Provide a human-readable message explaining what went wrong
  • Return all validation errors at once, not one at a time
  • Never expose stack traces, SQL errors, or internal system details in error responses
  • Include a requestId so server-side logs can be linked to a specific error

Leave a Comment