Skip to content

Choosing Status Codes

A handful of status codes cover almost every case. The skill is in the close calls — the pairs that are easy to confuse.

  • 200 OK — success with a body.
  • 201 Created — a resource was created; include a Location header.
  • 204 No Content — success, nothing to return (e.g. a DELETE).
  • 400 Bad Request — the request is malformed (bad JSON, missing required parts).
  • 401 Unauthorized — no/invalid credentials; the client must authenticate.
  • 403 Forbidden — authenticated, but not allowed.
  • 404 Not Found — no such resource.
  • 409 Conflict — the request conflicts with current state (duplicate, version clash).
  • 422 Unprocessable Entity — syntactically valid but semantically invalid (failed validation).
  • 429 Too Many Requests — rate limited.
  • 500 Internal Server Error / 503 Service Unavailable — the server failed.
  • 200 vs 201 vs 204 — returning a body → 200; created a resource → 201; nothing to say → 204.
  • 400 vs 422 — can the server even parse the request? Unparseable → 400; parsed fine but the values are invalid → 422.
  • 401 vs 403 — do we know who you are? Not authenticated → 401; authenticated but not permitted → 403.
  • 404 vs 409 — the resource does not exist → 404; it exists but your request conflicts with its state → 409.
JavaScript

Returning 200 with an error in the body Symptom:

  • HTTP 200 OK with { "success": false, "error": "Not found" }
  • Monitoring that watches HTTP status thinks the request succeeded
  • Alerts and SLA measurements go wrong
  • Use the right status code: 404, not 200

Using 500 for a client error Symptom:

  • Validation fails → the server throws → returns 500
  • The client can’t tell whether its own request was wrong or the server broke
  • Catch validation errors first → return 400 or 422

💡 Real-world examples

GitHub API:

  • 422 Unprocessable Entity for validation errors — a field breaks a rule
  • 409 Conflict for a duplicate resource
  • 403 Forbidden for an action you don’t have permission for (as opposed to 401, not authenticated at all)

Stripe API:

  • A full range of status codes: 400 invalid request, 401 auth failed, 402 payment failed, 429 rate limited
  • The HTTP status always maps to the error type

Sources: GitHub REST API errors, Stripe API — errors

The request body is valid JSON but a required field is missing. Best code?
The caller is authenticated but lacks permission. Which code?
A create request duplicates a resource that already exists. Which code fits?