WritingREST API Error Handling: Status Codes and Response Formats — LinkIT
6 min readapi-design, rest, error-handling, backend

REST API Error Handling: Status Codes and Response Formats

HTTP status codes, consistent error bodies, Problem Details, OpenAPI documentation, and failure reporting for asynchronous jobs.

API errors should tell a client what failed and whether it can correct or retry the request. Use status codes and a consistent response body so clients do not need endpoint-specific parsing.

Document stable error codes separately from human-readable messages so copy changes do not break client behavior.

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

Status codes help classify failures, but alerting should reflect impact. An unexpected surge in 401, 403, or other 4xx responses can signal an authentication regression or abuse; expected validation errors need different handling.

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 9457, which supersedes RFC 7807, defines Problem Details for HTTP APIs. It uses application/problem+json and fields such as type, title, status, detail, and instance.

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.

Discuss API error handling with LinkIT.