KAHIBARO
Discord Login Register

7.14. Error Responses

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:

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:

For example:

http
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
HTTP/1.1 400 Bad Request
Content-Type: text/plain
Something went wrong

The plain text version is hard for a client to parse and act on.

Recommended Fields

Here is a practical and common structure:

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."
    }
  ],
  "request_id": "b4f79c90-6b63-4fd4-97ef-ef5d13a17c01"
}

Field meanings:

FieldTypeDescription
errorstringShort, machine readable error code, like validation_error.
messagestringShort, human readable explanation.
detailsarrayOptional list of specific issues (fields, constraints, etc.).
request_idstringOptional 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:

Machine Readable Error Codes

The error field should be something your client code can switch on:

Example of how a frontend might use it:

js
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:

These are not helpful for clients.

Human Readable Messages

The message field is meant for developers or users. It should:

Good examples:

Bad examples:

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:

Simple Form Validation

Imagine a registration endpoint /api/users where the client sends:

json
{
  "email": "",
  "password": "123",
  "age": -1
}

A good error response might be:

http
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:

json
{
  "profile": {
    "first_name": "",
    "address": {
      "city": ""
    }
  }
}

Validation errors might look like:

json
{
  "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:

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 field and message.

4xx vs 5xx Error Responses

Error responses usually fall into two big groups:

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
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
  "error": "invalid_request",
  "message": "Query parameter 'page' must be a positive integer."
}
http
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
  "error": "invalid_token",
  "message": "Access token is missing or invalid."
}
http
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
  "error": "permission_denied",
  "message": "You are not allowed to delete this resource."
}
http
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:

Example:

http
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
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
Content-Type: application/json
{
  "error": "invalid_token",
  "message": "Access token is missing or invalid."
}

Variations:

The message should not reveal if a user exists or not. For example, on login:

http
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
  "error": "invalid_credentials",
  "message": "Invalid email or password."
}

Do not say:

This can help attackers guess valid accounts.

Forbidden Access

When the user is authenticated but not allowed to do something:

http
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:

json
{
  "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
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:

Some APIs also include headers like:

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:

Define a Single Error Schema

Pick a single error schema and use it everywhere.

For example, you might define:

json
{
  "error": "string",        // required
  "message": "string",      // required
  "details": "array",       // optional
  "request_id": "string"    // optional
}

And document it in your API docs, for example OpenAPI:

yaml
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
    - message

Then reuse it for all error responses:

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:

http
GET /api/users/9999 HTTP/1.1
Accept: application/json

Response:

http
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:

http
POST /api/users HTTP/1.1
Content-Type: application/json
{
  "email": "alice@example.com",
  "password": "VeryStrongPassword123"
}

If that email is already used:

http
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:

http
POST /api/users HTTP/1.1
Content-Type: application/json
{
  "email": "alice@example.com",
  "password": "abc"  // missing quotes, invalid JSON
}

Response:

http
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:

http
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
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:

Example of a bad response:

http
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:

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:

json
{
  "error": "invalid_credentials",
  "message": "Invalid email or password."
}

Do not send:

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:

json
{
  "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.

Common pattern:

  1. Generate a unique request_id for each request
  2. Attach it to logs for that request
  3. Return request_id in all error responses

Example:

http
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:

text
[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:

Example Test Cases

For a registration endpoint /api/users:

ScenarioExpected statusExpected error code
Missing email400validation_error
Password too short400validation_error
Email already exists409email_already_taken
Invalid JSON body400invalid_json
Database down500internal_error
Unauthorized access to /api/admin/users403permission_denied

You can write automated tests that:

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:

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

Comments

Please login to add a comment.

Don't have an account? Register now!