> ## Documentation Index
> Fetch the complete documentation index at: https://archie.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Error handling

> The REST API uses RFC 9457 Problem Details for every error response. Predictable shape, machine-readable types, field-level details.

The REST API uses [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457) for every error. Every error response has `Content-Type: application/problem+json` and a consistent JSON shape.

## Error response format

```json theme={null}
{
  "type": "/errors/unique-constraint",
  "title": "Conflict",
  "status": 409,
  "detail": "Unique constraint \"students_email_key\" violated. Key (email)=(alice@example.com) already exists.",
  "instance": "/api/rest/students",
  "errors": [
    { "field": "email", "rule": "unique", "message": "The value for email already exists" }
  ]
}
```

| Field      | Type   | What it tells you                                                                           |
| ---------- | ------ | ------------------------------------------------------------------------------------------- |
| `type`     | String | A stable, machine-readable URI identifying the error category. Switch on this in your code. |
| `title`    | String | A short human-readable summary, paired with the HTTP status.                                |
| `status`   | Number | The HTTP status code, duplicated in the body for convenience.                               |
| `detail`   | String | A human-readable explanation specific to this occurrence.                                   |
| `instance` | String | The request path that produced the error.                                                   |
| `errors`   | Array  | Field-level details when applicable (validation, constraints).                              |

## Status code reference

### 4xx — client errors

| Status | Type                             | Meaning                                  |
| ------ | -------------------------------- | ---------------------------------------- |
| 400    | `/errors/bad-request`            | Malformed request.                       |
| 400    | `/errors/missing-project-id`     | Missing `X-Project-ID` header.           |
| 400    | `/errors/complexity-limit`       | Query exceeded depth or relation limits. |
| 401    | `/errors/unauthorized`           | Missing or invalid auth token.           |
| 403    | `/errors/forbidden`              | Authenticated, but not allowed.          |
| 404    | `/errors/item-not-found`         | Record does not exist.                   |
| 404    | `/errors/not-found`              | Resource not found (composite PK, etc.). |
| 404    | `/errors/table-not-found`        | Table doesn't exist in the schema.       |
| 405    | `/errors/view-is-readonly`       | Tried to write to a view.                |
| 409    | `/errors/unique-constraint`      | Duplicate value on a unique field.       |
| 422    | `/errors/validation-failed`      | Body or parameters fail validation.      |
| 422    | `/errors/foreign-key-constraint` | Referenced record doesn't exist.         |
| 422    | `/errors/not-null-constraint`    | Required field is missing or null.       |
| 422    | `/errors/check-constraint`       | Field value violates a check constraint. |
| 429    | `/errors/rate-limit-exceeded`    | Too many requests.                       |

### 5xx — server errors

| Status | Type                     | Meaning                             |
| ------ | ------------------------ | ----------------------------------- |
| 500    | `/errors/internal-error` | Unexpected server error.            |
| 504    | `/errors/query-timeout`  | Database query exceeded time limit. |

## Common errors

### Validation (422)

Required fields missing or wrong types.

```json theme={null}
{
  "type": "/errors/validation-failed",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "One or more fields failed validation",
  "instance": "/api/rest/students",
  "errors": [
    { "field": "email", "rule": "required", "message": "The field 'email' is required" },
    { "field": "age", "rule": "type", "message": "Expected number, got string" }
  ]
}
```

### Unique constraint (409)

Duplicate value on a unique column. The `errors[].field` tells you which column.

```json theme={null}
{
  "type": "/errors/unique-constraint",
  "title": "Conflict",
  "status": 409,
  "detail": "Unique constraint \"students_email_key\" violated. Key (email)=(alice@example.com) already exists.",
  "instance": "/api/rest/students",
  "errors": [
    { "field": "email", "rule": "unique", "message": "The value for email already exists" }
  ]
}
```

For handling this without the read-then-write race condition, use a GraphQL [upsert](/features/backend/graphql-api-explorer/standard-operations#atomic-upserts).

### Foreign key (422)

Referencing a related record that doesn't exist.

```json theme={null}
{
  "type": "/errors/foreign-key-constraint",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "Foreign key constraint violated. Referenced record does not exist.",
  "instance": "/api/rest/students",
  "errors": [
    { "field": "cityId", "rule": "foreign_key", "message": "Referenced record in 'cities' does not exist" }
  ]
}
```

### Not found (404)

```json theme={null}
{
  "type": "/errors/item-not-found",
  "title": "Not Found",
  "status": 404,
  "detail": "Record not found",
  "instance": "/api/rest/students/00000000-0000-0000-0000-000000000000"
}
```

### Rate limit exceeded (429)

```json theme={null}
{
  "type": "/errors/rate-limit-exceeded",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit exceeded. Try again in 30 seconds.",
  "instance": "/api/rest/students"
}
```

The response includes rate-limit headers — back off using `Retry-After`:

| Header                  | What it tells you                         |
| ----------------------- | ----------------------------------------- |
| `Retry-After`           | Seconds to wait before retrying.          |
| `X-RateLimit-Limit`     | Requests allowed per window.              |
| `X-RateLimit-Remaining` | Requests remaining in the current window. |
| `X-RateLimit-Reset`     | Seconds until the window resets.          |

### Query timeout (504)

```json theme={null}
{
  "type": "/errors/query-timeout",
  "title": "Gateway Timeout",
  "status": 504,
  "detail": "Database query exceeded the maximum execution time",
  "instance": "/api/rest/students"
}
```

Tighten the filter, paginate, add an [index](/features/backend/data-model/indexes), or move to a [view](/features/backend/data-model/views) that pre-computes the result.

## Field names in errors

Database column names are converted to camelCase in error messages — a constraint on `created_at` reports the field as `createdAt`. This matches what you send and receive in JSON bodies.

## Handling errors in code

Switch on `status` first, then on `type` for ambiguous statuses:

| Status | What to do                                                                    |
| ------ | ----------------------------------------------------------------------------- |
| 400    | Fix the request. Don't retry.                                                 |
| 401    | Refresh the auth token. Retry once.                                           |
| 403    | Surface a "no permission" message. Don't retry — same user, same outcome.     |
| 404    | Treat as the resource not existing.                                           |
| 409    | Show the user a "duplicate" message. For automatable cases, switch to upsert. |
| 422    | Surface field-level errors back to the user. Don't retry the same payload.    |
| 429    | Honor `Retry-After` with exponential backoff.                                 |
| 5xx    | Retry with backoff. If persistent, alert.                                     |

```javascript theme={null}
async function call(url, init) {
  const res = await fetch(url, init);
  if (!res.ok) {
    const problem = await res.json();
    switch (problem.type) {
      case "/errors/unique-constraint":
        throw new DuplicateError(problem.errors?.[0]?.field);
      case "/errors/rate-limit-exceeded":
        const wait = parseInt(res.headers.get("Retry-After") ?? "1", 10);
        await new Promise((r) => setTimeout(r, wait * 1000));
        return call(url, init);
      default:
        throw new ApiError(problem);
    }
  }
  return res.json();
}
```

## FAQ

<AccordionGroup>
  <Accordion title="Why are validation errors 422 and not 400?">
    `400` means the request itself was malformed (bad JSON, wrong content type). `422` means the request was syntactically fine but semantically invalid — required fields missing, types wrong, constraints violated. The distinction tells your code whether to retry the same payload or fix the request shape first.
  </Accordion>

  <Accordion title="What's the difference between 401 and 403?">
    `401` means "we don't know who you are" — the auth token is missing, expired, or invalid. `403` means "we know who you are, but you can't do this" — the role doesn't have the necessary permission. Refresh tokens for `401`; surface a permission error for `403`.
  </Accordion>

  <Accordion title="Should I retry on 5xx?">
    Yes, with exponential backoff. Most 5xx errors are transient. Cap retries at 3–5 and alert if they keep failing.
  </Accordion>

  <Accordion title="Can I get more detail than `detail`?">
    The `errors` array gives field-level reasons. For complex cases, the `instance` URL points to the request that failed — useful for support tickets.
  </Accordion>

  <Accordion title="What happens to a `Idempotency-Key` request when it errors?">
    2xx and 4xx responses are cached and returned on retry with the same key. 5xx responses are not cached, so retries actually re-run the operation. See [Idempotency](/features/backend/rest-api-explorer/idempotency).
  </Accordion>
</AccordionGroup>
