7.14. Error Responses
Table of Contents
Why Error Responses Matter
When something goes wrong in your API, the way you report the error is as important as the success responses. Good error responses make it:
- Easy for frontend developers to handle problems
- Easier to debug issues in development and production
- Possible for clients to recover automatically from some failures
Poor error responses lead to confused users, fragile clients, and hard to maintain systems.
In this chapter, you will learn how to design clear, consistent, and machine friendly error responses for REST APIs.
A good API must return:
- The correct HTTP status code, and
- A well structured error body with enough information to understand the problem.
We will focus on the response body here, since HTTP status codes are covered in the dedicated chapter.
Basic Structure of an Error Response
A typical JSON error response from a REST API has at least:
- A machine readable code
- A human readable message
For example:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "invalid_request",
"message": "The 'email' field is required."
}This is much better than returning plain text like:
HTTP/1.1 400 Bad Request
Content-Type: text/plain
Something went wrongThe plain text version is hard for a client to parse and act on.
Recommended Fields
Here is a practical and common structure:
{
"error": "validation_error",
"message": "Some fields are invalid.",
"details": [
{
"field": "email",
"message": "Email is required."
},
{
"field": "password",
"message": "Password must be at least 8 characters."
}
],
"request_id": "b4f79c90-6b63-4fd4-97ef-ef5d13a17c01"
}Field meanings:
| Field | Type | Description |
|---|---|---|
error | string | Short, machine readable error code, like validation_error. |
message | string | Short, human readable explanation. |
details | array | Optional list of specific issues (fields, constraints, etc.). |
request_id | string | Optional id for this request, useful to locate logs on the server side. |
Rule: Pick a single, consistent error response schema for your API and use it everywhere, for all endpoints and error codes.
Choosing Error Codes vs Messages
There are two layers of information:
- Error code: for machines
- Message: for humans
Machine Readable Error Codes
The error field should be something your client code can switch on:
invalid_credentialsemail_already_takenresource_not_foundrate_limit_exceededpermission_deniedvalidation_errorinternal_error
Example of how a frontend might use it:
if (response.status === 400 && responseBody.error === "validation_error") {
// highlight invalid fields in a form
}
if (response.status === 401 && responseBody.error === "invalid_credentials") {
// show "wrong email or password"
}Avoid using vague or unstable error codes like:
unknownerrorsomething_wrong
These are not helpful for clients.
Human Readable Messages
The message field is meant for developers or users. It should:
- Be short and clear
- Not expose sensitive data
- Not leak internal details, such as SQL queries or stack traces
Good examples:
"Invalid email or password.""User with given id was not found.""You are not allowed to access this resource."
Bad examples:
"NullPointerException in UserService at line 42""SELECT * FROM users WHERE id=... failed: connection reset by peer"
Internal details belong in logs, not in API responses.
Validation Error Responses
Validation errors are very common in REST APIs. You need a structure that can:
- Handle multiple fields at once
- Handle nested structures
- Be easy to display in a form
Simple Form Validation
Imagine a registration endpoint /api/users where the client sends:
{
"email": "",
"password": "123",
"age": -1
}A good error response might be:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "validation_error",
"message": "Some fields are invalid.",
"details": [
{
"field": "email",
"message": "Email is required."
},
{
"field": "password",
"message": "Password must be at least 8 characters."
},
{
"field": "age",
"message": "Age must be greater than or equal to 0."
}
]
}
The client can simply loop over details and highlight each invalid field.
Nested Field Paths
For nested JSON structures, use a dotted path or JSON pointer:
{
"profile": {
"first_name": "",
"address": {
"city": ""
}
}
}Validation errors might look like:
{
"error": "validation_error",
"message": "Some fields are invalid.",
"details": [
{
"field": "profile.first_name",
"message": "First name is required."
},
{
"field": "profile.address.city",
"message": "City is required."
}
]
}Alternative path formats:
"profile/address/city"["profile", "address", "city"]
Choose one format and use it everywhere.
Rule: For validation errors, always include:
- A top level error code such as
validation_error, and - A list of field errors with
fieldandmessage.
4xx vs 5xx Error Responses
Error responses usually fall into two big groups:
- Client errors (4xx)
- Server errors (5xx)
You already learn which HTTP codes to use in the HTTP Status Codes chapter. Here we focus on what to return in the body.
Client Error Responses (4xx)
Client errors mean the request is wrong or not allowed.
Typical examples:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "invalid_request",
"message": "Query parameter 'page' must be a positive integer."
}HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": "invalid_token",
"message": "Access token is missing or invalid."
}HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": "permission_denied",
"message": "You are not allowed to delete this resource."
}HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": "resource_not_found",
"message": "User with id '1234' was not found."
}Server Error Responses (5xx)
Server errors mean the request was probably valid, but something failed on the server.
For security reasons:
- Use generic messages
- Do not include stack traces or SQL errors
- Always log internal details on the server
Example:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"error": "internal_error",
"message": "An unexpected error occurred. Please try again later.",
"request_id": "4ffdbd2d-4e93-4b3d-bd5d-7b73e3e7560f"
}
The request_id helps you find the exact error in your logs.
Handling Authentication and Authorization Errors
Authentication and authorization errors appear very often in APIs.
Missing or Invalid Credentials
When the user is not authenticated:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
Content-Type: application/json
{
"error": "invalid_token",
"message": "Access token is missing or invalid."
}Variations:
"error": "token_expired""error": "token_revoked""error": "authentication_required"
The message should not reveal if a user exists or not. For example, on login:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": "invalid_credentials",
"message": "Invalid email or password."
}Do not say:
"User not found""Password is incorrect"
This can help attackers guess valid accounts.
Forbidden Access
When the user is authenticated but not allowed to do something:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"error": "permission_denied",
"message": "You do not have permission to access this resource."
}You might include extra safe information for debugging:
{
"error": "permission_denied",
"message": "You do not have permission to access this resource.",
"details": [
{
"required_role": "admin",
"user_role": "user"
}
]
}Return this only if you are comfortable exposing that level of detail.
Error Responses for Rate Limiting and Throttling
If you implement rate limiting, you should return consistent error responses so clients can back off correctly.
Typical response:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{
"error": "rate_limit_exceeded",
"message": "Too many requests. Please try again later.",
"details": [
{
"limit": 100,
"window": "60s",
"remaining": 0
}
]
}Here:
429tells the client they are sending too many requestsRetry-Afterindicates when to retry- The JSON body gives more detail for UI or logs
Some APIs also include headers like:
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset
These are covered in more detail in the Rate Limiting chapter.
Consistency Across the API
The most important characteristic of a good error design is consistency.
Clients should not have to guess:
- Different shapes for different endpoints
- Different fields for 4xx and 5xx
- Some errors as JSON, some as plain text, some as HTML
Define a Single Error Schema
Pick a single error schema and use it everywhere.
For example, you might define:
{
"error": "string", // required
"message": "string", // required
"details": "array", // optional
"request_id": "string" // optional
}And document it in your API docs, for example OpenAPI:
Error:
type: object
properties:
error:
type: string
description: Machine readable error code
message:
type: string
description: Human readable error message
details:
type: array
items:
type: object
description: Optional list of detailed error information
request_id:
type: string
description: Optional id used to trace the request in logs
required:
- error
- messageThen reuse it for all error responses:
- 400 Bad Request
- 401 Unauthorized
- 403 Forbidden
- 404 Not Found
- 409 Conflict
- 422 Unprocessable Entity
- 429 Too Many Requests
- 500 Internal Server Error
Rule: All your REST endpoints should, as much as possible, return errors with the same JSON format. Only the error, message, and details content should change.
Examples of Common Error Scenarios
To make these ideas concrete, here are several complete examples.
Example 1: Resource Not Found
Request:
GET /api/users/9999 HTTP/1.1
Accept: application/jsonResponse:
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": "resource_not_found",
"message": "User with id '9999' was not found."
}Example 2: Unique Constraint Violation
Request:
POST /api/users HTTP/1.1
Content-Type: application/json
{
"email": "alice@example.com",
"password": "VeryStrongPassword123"
}If that email is already used:
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"error": "email_already_taken",
"message": "A user with this email already exists."
}The server will log the actual database error internally.
Example 3: Invalid JSON Body
Request:
POST /api/users HTTP/1.1
Content-Type: application/json
{
"email": "alice@example.com",
"password": "abc" // missing quotes, invalid JSON
}Response:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "invalid_json",
"message": "Request body is not valid JSON."
}You can also include line/column info if you want, but be careful not to break the JSON.
Example 4: Business Rule Violation
Request:
POST /api/orders HTTP/1.1
Content-Type: application/json
{
"user_id": 123,
"items": [
{ "product_id": "p1", "quantity": 1000 }
]
}If your business rule is "max 100 units per item":
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "business_rule_violation",
"message": "Order could not be created due to business rule violation.",
"details": [
{
"code": "max_quantity_exceeded",
"field": "items[0].quantity",
"message": "Maximum quantity for a single item is 100."
}
]
}Clients can then show a friendly message and adjust the quantity.
Security Considerations for Error Responses
Error responses can unintentionally reveal sensitive information to attackers.
Here are some important guidelines.
Do Not Leak Internals
Avoid:
- Database names or table names
- SQL queries
- Stack traces with file paths
- Internal IPs or hostnames
- Exact reasons why authentication failed
Example of a bad response:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"error": "SQLException",
"message": "SQLSTATE[42S22]: Column not found: 1054 Unknown column 'pasword' in 'field list'",
"stack_trace": "...long stack trace..."
}This leaks:
- The database type
- The actual SQL query structure
- Code structure of your application
Instead, log this on the server, and respond with a generic message.
Avoid Overly Detailed Auth Errors
During login, return a generic message such as:
{
"error": "invalid_credentials",
"message": "Invalid email or password."
}Do not send:
"No user found with this email.""Password incorrect."
This makes it easier to enumerate valid accounts.
Do Not Echo Untrusted Data Without Sanitization
Even in error messages, do not blindly include user input without escaping or filtering. For example:
{
"error": "invalid_request",
"message": "Invalid value: <script>alert(1)</script>"
}
If your client directly injects message into HTML without escaping, this can introduce XSS issues.
Rule: Error responses should be:
- Helpful, but not overly detailed
- Safe to display to end users
- Free from internal technical details and raw user input
Logging vs Error Responses
Your error response is for the client. Your logs are for the server operators. They should contain different levels of detail.
- Error responses
- Clear, but high level
- No secrets
- No internal implementation details
- Logs
- Full stack trace
- Database errors
- Context information
- Sensitive data only when strictly necessary and protected
Common pattern:
- Generate a unique
request_idfor each request - Attach it to logs for that request
- Return
request_idin all error responses
Example:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"error": "internal_error",
"message": "An unexpected error occurred. Please try again later.",
"request_id": "7b6c884b-9c7e-4bd1-8acb-8a8cb6f0bc4b"
}In your logs:
[request_id=7b6c884b-9c7e-4bd1-8acb-8a8cb6f0bc4b] ERROR: Null pointer in OrderService at line 123...This helps support teams investigate user issues.
Testing Error Responses
Testing your error responses is just as important as testing success responses.
You want to ensure that:
- Correct status codes are returned
- Error bodies follow your schema
errorfields use the documented codesdetailsare present for validation errors
Example Test Cases
For a registration endpoint /api/users:
| Scenario | Expected status | Expected error code |
|---|---|---|
Missing email | 400 | validation_error |
| Password too short | 400 | validation_error |
| Email already exists | 409 | email_already_taken |
| Invalid JSON body | 400 | invalid_json |
| Database down | 500 | internal_error |
Unauthorized access to /api/admin/users | 403 | permission_denied |
You can write automated tests that:
- Send invalid requests
- Check status codes
- Validate that the JSON has
errorandmessagefields - Verify that validation errors have a
detailsarray
This ensures your API behaves consistently, and does not accidentally leak sensitive information.
Summary
You have seen how to design effective error responses for REST APIs.
Key ideas:
- Use the right HTTP status code plus a JSON body
- Include a machine readable error code and a human readable message
- For validation, always provide field specific details
- Keep a consistent schema for all errors across your API
- Be careful not to leak internal details or secrets
- Use
request_idto link error responses to server logs - Test your error responses just like your success responses
A well designed REST API:
- Uses consistent JSON error structures,
- Returns correct status codes,
- And gives clients enough information to understand and recover from errors, without exposing internal details.
These principles apply regardless of which framework or language you use. In later chapters, you will see how to implement such error responses concretely with tools like FastAPI.
Views: 17
KAHIBARO