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:
- What went wrong?
- Why did it go wrong?
- 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
