# Errors

Every Hangar error response is a JSON object with a stable code, a
human message, and an optional structured detail.

## Shape

```json
{
  "error": "<machine-readable-code>",
  "message": "<human description>",
  "details": { "...": "optional" },
  "retryAfterSeconds": 0
}
```

The `error` code is machine-readable and stable across releases. The
`message` is for human display. `details` carries Zod-style field
errors on validation failures; `retryAfterSeconds` is set on 429 and
mirrors the `Retry-After` header.

## Code catalog

| Code | HTTP | When to expect it | What to do |
|---|---|---|---|
| `unauthorized` | 401 | Missing, malformed, or expired token / session. | Re-authenticate; mint a new PAT if expired. |
| `forbidden` | 403 | Token is valid but lacks the required scope. | Mint a token that includes the scope. |
| `not-found` | 404 | Resource doesn't exist for this caller. | Check the id; resource may belong to another user. |
| `validation` | 400 | Request body or query failed Zod validation. | Inspect `details` for field-level errors and fix the request. |
| `invalid-json` | 400 | Body could not be parsed as JSON. | Send a valid JSON body with `Content-Type: application/json`. |
| `rate-limited` | 429 | Sliding-window limit exceeded. | Honor `retryAfterSeconds` and back off. |
| `wallet-empty` | 402 | Wallet hit zero before the call ran. | Top up via `POST /api/wallet/topup` and retry. |
| `provider-error` | 502 | Upstream LLM or billing provider failed. | Retry with exponential backoff (4s, 8s, 16s, 32s). |
| `missing-id` | 400 | A required path or query parameter is missing. | Add the parameter and retry. |
| `conflict` | 409 | Resource state conflicts with the requested change. | Re-read state; the operation is not safe to retry blindly. |
| `internal` | 500 | Unhandled server error. | Retry once; report on Discussions if it persists. |

## Examples

### Validation

```json
{
  "error": "validation",
  "message": "Request validation failed.",
  "details": {
    "name": ["String must contain at least 1 character(s)"]
  }
}
```

### Wallet empty

```json
{
  "error": "wallet-empty",
  "message": "Wallet balance is zero. Top up at /dashboard/wallet."
}
```

### Rate limited

```json
{
  "error": "rate-limited",
  "message": "Too many requests. Retry after 12s.",
  "retryAfterSeconds": 12
}
```

## What never happens

- We never return HTML on a 4xx/5xx response from `/api/*`. Always JSON.
- We never include a stack trace or PII in production responses.
- We never surface upstream provider error bodies verbatim — they are
  re-mapped to `provider-error` with a redacted `details` field.
Error catalog — Hangar