When an API request fails, the HTTP status code says what kind of failure happened, and the JSON body should say what exactly went wrong and what the client can do about it. A consistent error shape across every endpoint makes client code simpler, makes logs searchable, and turns support requests into quick lookups. This guide covers practical patterns.
VALIDATION_FAILED, that client code can branch on. Never make clients parse the message text.json{ "error": { "code": "UNAUTHORIZED", "message": "Missing or invalid token" } }
Use the same envelope for every error from every endpoint, even when there are no details to add.
json{ "error": { "code": "VALIDATION_FAILED", "message": "Some fields are invalid", "details": [ { "field": "email", "issue": "invalid_format" }, { "field": "age", "issue": "must_be_greater_than_or_equal_to_18" } ] } }
Return all validation problems at once rather than only the first, so users can fix everything in one pass. For nested input, use a path in field, such as "items[2].quantity" or a JSON Pointer like "/items/2/quantity".
json{ "error": { "code": "INTERNAL", "message": "Unexpected error" }, "traceId": "b4f8f8e7d7d44b1a" }
For server errors, the client can't fix anything. What helps is a trace ID that support can search for in your logs. Return the same value in a response header such as X-Request-Id, so it's available even when the body can't be parsed.
| Status | When to use it | Example code |
|---|---|---|
| 400 Bad Request | Malformed JSON or wrong types | INVALID_JSON |
| 401 Unauthorized | Missing or invalid credentials | UNAUTHORIZED |
| 403 Forbidden | Authenticated but not allowed | FORBIDDEN |
| 404 Not Found | The resource doesn't exist | NOT_FOUND |
| 409 Conflict | State conflict, such as a duplicate or stale version | CONFLICT |
| 422 Unprocessable Content | Valid JSON that fails business rules | VALIDATION_FAILED |
| 429 Too Many Requests | Rate limit exceeded; add Retry-After | RATE_LIMITED |
| 500 Internal Server Error | Unexpected failure on the server | INTERNAL |
| 503 Service Unavailable | Temporary outage or maintenance | UNAVAILABLE |
Teams differ on 400 versus 422 for validation errors. Pick one and use it consistently.
If you'd rather not design your own envelope, RFC 9457 defines a standard error format served as application/problem+json:
json{ "type": "https://example.com/problems/insufficient-funds", "title": "Insufficient funds", "status": 403, "detail": "Your balance is 30, but the transfer costs 50.", "instance": "/transfers/12345" }
You can add your own fields, such as code, errors or traceId. Many frameworks support it out of the box.
/users returns {"error": "..."} and /orders returns {"message": "..."}, every client needs special cases.Send deliberately bad requests with curl or your API testing tool: a missing token, a malformed body, a wrong type. Check that the status code and the error shape match your conventions, and add automated tests that assert on error.code. See Best practices for testing REST APIs and API JSON best practices.