API Error Response Design: Best Practices Your Consumers Will Thank You For
Most APIs are built with the happy path in mind. The endpoint works, the data comes back, and everyone is satisfied. Error handling tends to get bolted on afterwards, inconsistently, across different developers and different sprints, until the API has six different error response formats depending on which endpoint failed and why.
For any developer integrating with your API, error responses are often more important than success responses. A well-formed error tells them exactly what went wrong, what they can do about it, and whether they should retry. A poorly-formed error sends them to your documentation, then to your support inbox, then to a frustrated LinkedIn post about your developer experience.
Here is how to get it right.
Use HTTP Status Codes Correctly
HTTP status codes exist for a reason. Using them correctly means your consumers can handle errors programmatically without parsing the response body first.
The most common mistakes:
- Returning 200 for everything - including errors. If the operation failed, the status code should reflect that. A 200 response with
{"success": false}in the body forces every consumer to check the body before they know whether the call worked. - Using 500 for everything that goes wrong - a 500 should mean an unexpected server error. Validation failures are 422. Missing resources are 404. Unauthorised requests are 401 or 403. Use the right code.
- Confusing 401 and 403 - 401 means the request lacks valid authentication credentials (try logging in). 403 means the credentials are valid but the user does not have permission (do not bother trying again with the same account).
The codes your API will use most often:
400- Bad request (malformed syntax, missing required fields)401- Unauthenticated (no credentials, or invalid credentials)403- Forbidden (authenticated but not authorised)404- Not found409- Conflict (duplicate resource, version mismatch)422- Unprocessable entity (validation failed)429- Too many requests (rate limited)500- Internal server error (unexpected failure)503- Service unavailable (maintenance, downstream dependency down)
Use a Consistent Error Response Schema
Whatever structure you choose, use it everywhere. The worst thing you can do is return a different error format from each endpoint or controller. Consumers have to write different error-handling logic for each one.
The RFC 9457 Problem Details format is the standard worth knowing. It gives you a defined set of fields that cover most scenarios:
type- a URI identifying the error type (can be a docs URL or a stable identifier)title- a short, human-readable summary of the problemstatus- the HTTP status code (repeating it in the body makes it easier to log and process)detail- a human-readable explanation of this specific occurrenceinstance- a URI reference identifying the specific occurrence (useful for support)
A basic example:
{
"type": "https://theapiguys.co.uk/errors/validation-failed",
"title": "Validation Failed",
"status": 422,
"detail": "The email field must be a valid email address.",
"instance": "/api/users/register"
}
You do not have to implement RFC 9457 exactly - but the principle of having a stable, documented schema is non-negotiable if you want your API to be usable.
Validation Errors Need Field-Level Detail
A generic "validation failed" message is not enough when a form has twenty fields. Your 422 responses should tell the consumer exactly which fields failed and why.
{
"type": "https://theapiguys.co.uk/errors/validation-failed",
"title": "Validation Failed",
"status": 422,
"errors": {
"email": ["The email field is required."],
"password": ["The password must be at least 8 characters.", "The password must contain a number."]
}
}
In Laravel, the default validation exception response already provides field-level errors. If you are building a JSON API, make sure your exception handler returns these in a consistent structure rather than the default Laravel format, which varies slightly between versions.
Never Leak Internals
Error responses in production must never expose:
- Stack traces
- SQL queries or database error messages
- File paths from your server
- Dependency versions
- Internal service names or IP addresses
All of this is useful to an attacker and useless to a legitimate consumer. Log the full detail server-side where it is safe. Return only what the consumer needs to understand and handle the error.
In Laravel, ensure APP_DEBUG is false in production and that your exception handler renders a sanitised response rather than the full debug output. This is the default behaviour when debug mode is off, but it is worth verifying in your deployment pipeline.
Include a Correlation ID
When an error occurs, give the consumer a reference they can include in a support request. A correlation ID (sometimes called a request ID or trace ID) is a unique identifier generated per request that links the consumer's error to your server logs.
{
"type": "https://theapiguys.co.uk/errors/internal-error",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred. Please contact support with the request ID.",
"request_id": "01HXYZ4J6K8MNTQRV2W3"
}
Return the same ID in a response header (X-Request-Id) so it can be captured even if the consumer does not parse the body. This transforms "it's broken" support tickets into actionable log lookups.
Be Helpful About Retries
Some errors are worth retrying. Others are not. Make it obvious which is which.
For rate limiting (429), return a Retry-After header with the number of seconds the consumer should wait. For service unavailability (503), do the same. For validation failures (422) or not-found errors (404), there is no point retrying - tell the consumer so, either through documentation or through a field in the response body.
For transient errors, a retryable field in the response body is a courteous addition:
{
"status": 503,
"title": "Service Temporarily Unavailable",
"retryable": true,
"retry_after": 30
}
Document Your Errors as Thoroughly as Your Endpoints
Every endpoint in your API documentation should list the error responses it can return, not just the success response. If an endpoint can return a 403, document what triggers it. If it returns a 409, explain what the conflict condition is.
Developers integrating with your API will encounter errors before they encounter most success responses. If your documentation only covers the happy path, they are on their own the moment something goes wrong.
If you are using OpenAPI to describe your API, the responses object supports error status codes natively. There is no excuse for leaving them out. We have written about API authentication and rate limiting before - error design sits alongside both as a non-negotiable part of building APIs that people can actually rely on.
A Question Worth Asking Your Team
Take a look at your API's error responses right now - not the documentation, the actual responses. Pick three endpoints and trigger an error on each one. Are the response structures consistent? Does the status code accurately reflect what went wrong? Is there anything in the response body that should not be there?
Most teams that do this exercise find at least one endpoint returning a 500 where it should be a 422, one returning a stack trace in a non-production environment that made it to production, and one with a completely different error format inherited from a third-party library.
What is the worst API error response you have ever had to debug - and what made it so hard to work with?
