# Errors

## Envelope (every non-2xx response)

```json
{
  "error": {
    "code": "CONSTANT_CASE_CODE",
    "message": "Human-readable one-liner",
    "docs": "https://asyncbase.dev/docs/errors/CONSTANT_CASE_CODE"
  }
}
```

Status code matches HTTP semantics. Client libraries raise a typed
`AsyncBaseError` exposing `code`, `message`, `docs`, HTTP status, and
the `X-Request-Id` header.

## Every code

| HTTP | code | When |
|------|------|------|
| 400 | `QUEUE_NAME_INVALID` | queue name fails `^[a-zA-Z0-9_-]{1,128}$`; body validation; delay + dedupe_id combined; dedupe_id without fifo_group |
| 400 | `VALIDATION_FAILED` | request field fails schema validation (out-of-range, wrong type, unknown field) |
| 401 | `AUTH_INVALID` | missing/wrong Bearer, or key not in DB |
| 401 | `AUTH_KEY_EXPIRED` | key revoked or rotated past 1 h grace |
| 403 | `QUEUE_LIMIT_REACHED` | plan queue count exceeded (Free=3, Hobby=10, Starter=50, Pro=unlimited) |
| 403 | `QUEUE_PAUSED` | queue is paused; enqueue and consume rejected |
| 403 | `MESSAGE_LIMIT_REACHED` | monthly message quota exhausted (Free=100K, Hobby=2M, Starter=20M, Pro=50M) |
| 404 | `QUEUE_NOT_FOUND` | queue does not exist |
| 404 | `MESSAGE_NOT_FOUND` | msg_id not found or already acked/expired |
| 413 | `PAYLOAD_TOO_LARGE` | JSON payload > 256 KB |
| 429 | `RATE_LIMITED` | per-tenant req/s exceeded; carries `Retry-After: 1` + `X-RateLimit-*` headers |
| 500 | `INTERNAL_ERROR` | unexpected server issue |
| 503 | `INTERNAL_ERROR` | Postgres unreachable AND no stale cache |
| 503 | `SERVICE_UNAVAILABLE` | planned maintenance or complete dependency failure |

## Recommended client behavior

| Code | Retry? | Backoff |
|------|--------|---------|
| 429 | yes | honor `Retry-After`, else 1 s + jitter |
| 503 | yes | 1 s, 2 s, 4 s (cap 30 s) |
| 500 | once | 1 s |
| 4xx (others) | no | raise to caller |

## SDK error helpers

- Node: `err instanceof AsyncBaseError && err.code === "RATE_LIMITED"`
- Python: `except AsyncBaseError as e: if e.code == "RATE_LIMITED": ...`
- PHP: `catch (AsyncBaseError $e) if ($e->errorCode === 'RATE_LIMITED')` (PHP
  `Exception::$code` is int-only, so we use `$errorCode`)
- Go: `asyncbase.IsCode(err, "RATE_LIMITED")`
