Master API error response design, consistent error formats, validation errors, error codes, and best practices from real-world APIs like Stripe and GitHub.
Errors are inevitable. Users will send invalid data. Databases will go down. Third-party APIs will timeout. Networks will fail. The difference between a good API and a great API is how it handles these failures.
A well-designed error response tells the client:
A poorly-designed error response leaves the client guessing:
{
"error": "Something went wrong"
}What went wrong? Is it my fault or yours? Which field is invalid? What should I do?
Compare this to a well-designed error:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request data",
"details": [
{
"field": "email",
"message": "Email must be a valid email address",
"value": "not-an-email"
},
{
"field": "age",
"message": "Age must be at least 18",
"value": 15
}
]
}
}Now the client knows exactly what is wrong and how to fix it. This is the standard we should aim for.
The first rule of error handling is consistency. Every error response, regardless of type, should follow the same structure. This makes errors predictable and easy to handle programmatically.
Here is a recommended error response format:
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable summary",
"details": [ /* Optional: field-level errors */ ]
}
}The error object:
error key (not at the root)The code field:
VALIDATION_ERROR, RESOURCE_NOT_FOUND, RATE_LIMIT_EXCEEDED)The message field:
The details field (optional):
field, message, and optionally valueExamples:
404 Not Found:
HTTP/1.1 404 Not Found
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "User with ID 123 does not exist"
}
}401 Unauthorized:
HTTP/1.1 401 Unauthorized
{
"error": {
"code": "INVALID_TOKEN",
"message": "Authentication token is invalid or expired"
}
}500 Internal Server Error:
HTTP/1.1 500 Internal Server Error
{
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "An unexpected error occurred. Please try again later."
}
}Notice how each error, regardless of status code, follows the same structure. This consistency is crucial.
Validation errors are special because they often involve multiple fields failing multiple constraints. A user might submit a form with an invalid email, a password that is too short, and a missing required field — all in one request.
For these cases, use a 422 Unprocessable Entity status code and provide field-level details in the details array:
HTTP/1.1 422 Unprocessable Entity
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Email is required"
},
{
"field": "password",
"message": "Password must be at least 8 characters",
"value": "abc"
},
{
"field": "age",
"message": "Age must be a number",
"value": "not-a-number"
}
]
}
}Each item in the details array has:
field — The name of the field that failed (matches the request body field name)message — A human-readable error message specific to this fieldvalue (optional) — The invalid value that was submitted (helpful for debugging)This structure allows frontend applications to:
details arrayWhy not just return the first error? Some APIs return only the first validation error and require the client to fix it and resubmit to see the next error. This is frustrating for users. If the client submits a form with 5 invalid fields, tell them about all 5 at once, not one at a time.
400 vs 422 for validation errors?
400 Bad Request — The request is malformed (invalid JSON, wrong Content-Type)422 Unprocessable Entity — The request is well-formed but fails business logic validationBoth are acceptable, but 422 is more precise for validation errors.
Errors fall into two categories, and it is crucial to distinguish between them:
Client Errors (4xx) — The client made a mistake These errors are caused by invalid input, missing authentication, lack of permissions, or requesting a resource that doesn't exist. The client should fix the request and try again.
Common client errors:
400 Bad Request — Malformed request syntax401 Unauthorized — Missing or invalid authentication403 Forbidden — Authenticated but not authorized404 Not Found — Resource doesn't exist422 Unprocessable Entity — Validation failed429 Too Many Requests — Rate limit exceededWhen to use: The error is the client's fault and they can fix it.
Server Errors (5xx) — The server failed These errors are caused by bugs, crashes, database failures, or third-party service outages. The client did nothing wrong. The request might succeed if retried later.
Common server errors:
500 Internal Server Error — Unexpected error (crashed, uncaught exception)502 Bad Gateway — Upstream service returned an invalid response503 Service Unavailable — Server is temporarily down (maintenance, overload)504 Gateway Timeout — Upstream service didn't respond in timeWhen to use: The error is the server's fault and the client cannot fix it.
Critical distinction:
If a client sends {age: "not-a-number"}, that is a client error (422). If the database crashes while processing a valid request, that is a server error (500).
Clients can retry 5xx errors (with exponential backoff), but retrying 4xx errors without fixing the request is pointless.
Let's look at how industry-leading APIs handle errors.
Stripe (Payment API) Stripe has one of the best-designed error responses in the industry:
{
"error": {
"type": "card_error",
"code": "card_declined",
"message": "Your card was declined.",
"param": "exp_year",
"decline_code": "generic_decline"
}
}Notice:
type categorization (card_error, invalid_request_error, api_error)code for programmatic handlingmessageparam field to indicate which parameter caused the errordecline_code) for payment-specific errorsGitHub (REST API) GitHub uses a clean, consistent format:
{
"message": "Validation Failed",
"errors": [
{
"resource": "Issue",
"field": "title",
"code": "missing_field"
}
],
"documentation_url": "https://docs.github.com/rest/reference/issues#create-an-issue"
}Notice:
errors array for field-level detailsresource and field to pinpoint the problemdocumentation_url to help developers understand the APITwilio (SMS API) Twilio uses a simple, effective format:
{
"code": 21211,
"message": "The 'To' number is not a valid phone number.",
"more_info": "https://www.twilio.com/docs/errors/21211",
"status": 400
}Notice:
more_info URL linking to detailed error documentationstatus field (redundant with HTTP status, but explicit)Common patterns:
These APIs are used by millions of developers precisely because their errors are predictable, informative, and actionable.
A user submits a form to create an account, but the email field is missing and the password is too short. What should the API return?