Consistent Error Responses
Return predictable error shapes across all endpoints. Clients should parse one error format.
Standard Error Shape
{
"error": {
"code": "MACHINE_READABLE_CODE",
"message": "Human-readable description",
"details": []
}
}Error Codes
| HTTP Status | Error Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR |
Invalid input |
| 401 | UNAUTHENTICATED |
Missing or invalid auth |
| 403 | FORBIDDEN |
Authenticated but not authorized |
| 404 | NOT_FOUND |
Resource does not exist |
| 409 | CONFLICT |
Duplicate or state conflict |
| 429 | RATE_LIMITED |
Too many requests |
| 500 | INTERNAL_ERROR |
Unexpected server error |
Pattern
// Pseudocode — central error handler middleware
function errorHandler(error, request, response):
if error is ValidationError:
return 400, formatError("VALIDATION_ERROR", error.message, error.fieldErrors)
if error is AuthError:
return 401, formatError("UNAUTHENTICATED", "Invalid credentials")
if error is ForbiddenError:
return 403, formatError("FORBIDDEN", "Access denied")
if error is NotFoundError:
return 404, formatError("NOT_FOUND", error.message)
// Unexpected errors
log.error(error, { requestId: request.id })
return 500, formatError("INTERNAL_ERROR", "Something went wrong")
function formatError(code, message, details = null):
return { error: { code, message, details } }Rules
- Never leak stack traces, SQL errors, or file paths in production responses.
- Log full error details server-side with request ID.
- Return 404 (not 403) for missing resources to prevent enumeration.
- Include
requestIdin error response for support debugging.