WritingREST API Error Handling: HTTP Status Codes, Error Shapes, and What Clients Actually Need — Clixo
6 min readapi-design, rest, error-handling, backend

REST API Error Handling: HTTP Status Codes, Error Shapes, and What Clients Actually Need

A complete guide to REST API error handling — which HTTP status codes to use, how to structure error responses, and how to make errors actionable for API consumers.

Error handling is where most APIs reveal how thoughtfully they were designed. A well-handled error tells the client what went wrong, who is responsible for fixing it, and what to do next. A poorly handled error returns 500 Internal Server Error for a missing required field, or returns 200 OK with "success": false in the body.

Clients that consume poorly-designed errors write defensive code around every call, file support tickets, and eventually build their own error-handling wrappers. This is preventable.

HTTP Status Codes: The Right Ones to Use

The HTTP status code space has over 70 registered codes. Most APIs should use fewer than 15. Here are the ones that matter and what they actually communicate:

2xx — Success

  • 200 OK — the request succeeded and the response body contains the result
  • 201 Created — a new resource was created; the Location response header should point to the new resource URL
  • 204 No Content — the request succeeded and there is no response body (common for DELETE and some PUT operations)

4xx — Client Errors (the Client Can Fix These)

  • 400 Bad Request — the request is malformed; the body could not be parsed or a required parameter is absent
  • 401 Unauthorized — the request lacks valid authentication credentials; the client should authenticate and retry
  • 403 Forbidden — the client is authenticated but not authorized to perform this operation; retrying with the same credentials will not help
  • 404 Not Found — the resource does not exist at this URL
  • 405 Method Not Allowed — the HTTP method is not supported for this endpoint; the Allow header should list the supported methods
  • 409 Conflict — the request conflicts with current resource state (duplicate key, optimistic locking failure, state machine violation)
  • 410 Gone — the resource existed but has been permanently deleted; unlike 404, signals that caching and retrying are pointless
  • 422 Unprocessable Entity — the request body is syntactically valid but semantically invalid (validation errors on field values)
  • 429 Too Many Requests — the client has exceeded a rate limit; include Retry-After in the response

5xx — Server Errors (the Server Is Responsible)

  • 500 Internal Server Error — an unexpected server-side failure; the client should not retry immediately
  • 502 Bad Gateway — the server received an invalid response from an upstream dependency
  • 503 Service Unavailable — the server is temporarily unable to handle requests (overload, maintenance); include Retry-After
  • 504 Gateway Timeout — a timeout waiting for an upstream dependency

The 4xx vs 5xx distinction is operationally important: 4xx errors should not trigger alerts for on-call engineers because they are the client's problem. 5xx errors should. Getting this wrong means either alert fatigue (from expected 4xx noise) or silent failures (from 5xx miscategorized as 4xx).

REST API Error Response Shape

Every error response should have a consistent structure. The response body should tell the client:

  1. A machine-readable error code
  2. A human-readable description
  3. Which field or parameter caused the error, when applicable
  4. Any additional context that helps the client fix the problem

A practical and widely-used shape:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "code": "INVALID_FORMAT",
        "message": "email must be a valid email address"
      },
      {
        "field": "age",
        "code": "OUT_OF_RANGE",
        "message": "age must be between 18 and 120"
      }
    ]
  }
}

The code field matters for machine consumers — it is the string the client will switch on. The message is for humans reading logs or building a UI. The details array allows multiple validation errors to be reported in a single response rather than one error per round trip.

RFC 7807 (Problem Details) is a standardized format for HTTP error responses. It defines a Content-Type: application/problem+json and a standard set of fields (type, title, status, detail, instance). If you are building a public API, adopting RFC 7807 gives clients a familiar shape to work with and signals that you take API design seriously.

Common Error Handling Mistakes

Returning different error shapes on different endpoints. If half your API returns {"error": "..."} and the other half returns {"message": "..."}, clients have to handle both. Pick one shape and apply it everywhere.

Using 500 for validation errors. A missing required field is not a server error. Return 400 or 422 with a clear message identifying which field is missing. A 500 for this case fills your error monitoring with noise and makes it harder to distinguish real server bugs from client mistakes.

Not including error codes, only messages. Messages are for humans. Error codes are for code. Clients that parse error messages to decide what to do are brittle — a message rephrasing becomes a breaking change. Use stable, documented codes.

Generic error messages that do not help. "An error occurred" tells the client nothing. "email is required" lets them fix the problem. The marginal effort of writing specific messages is worth it.

Leaking internal details in 500 errors. Stack traces, database error messages, and internal service names should never appear in API responses. Log them internally. Return a safe, generic message to the client.

Inconsistent nullability in error fields. If the field property in an error detail is sometimes present, sometimes null, and sometimes absent, clients have to handle all three cases. Make nullability explicit and consistent.

Documenting Errors in Your OpenAPI Spec

Document error responses as carefully as success responses. For each endpoint:

  • List every 4xx status code the endpoint can return and what triggers it
  • Define the error response schema (not just a description string)
  • Provide examples for at least the most common error cases

Clients should be able to read your spec and know every error they need to handle, without discovering them by hitting the API.

Errors in Asynchronous Operations

For long-running operations that return immediately with a job ID and complete asynchronously, errors require additional thought. The 202 Accepted that starts the job was a success. If the job fails later, the error needs to be communicated through the job status resource:

GET /jobs/abc123
{
  "status": "failed",
  "error": {
    "code": "INSUFFICIENT_FUNDS",
    "message": "Payment could not be processed"
  }
}

Design the failed state schema for async jobs as carefully as the error responses for synchronous endpoints.

For teams building APIs where error design, observability, and client experience are first-class requirements, Clixo designs and ships production backend systems where these patterns are enforced from the start.