# Errors

> Every error is an RFC 9457 problem document with a stable type, a sentence for people and a path for every invalid field.

Source: https://shopfa.st/docs/guides/errors

Any non-2xx answer has `content-type: application/problem+json`:

```json
{
  "type": "https://shopfa.st/problems/validation",
  "title": "Validation failed",
  "status": 422,
  "detail": "The request body is invalid.",
  "fields": [
    { "path": "body.priceMinor", "message": "Too small: expected number to be >=0" }
  ]
}
```

- **`type`** is stable: branch on its last segment (`validation`, `forbidden`, `rate-limited`, `edit-conflict`…). It is
  an identifier, not a page to fetch.
- **`detail`** is one plain sentence you can show to a person.
- **`fields`** lists every invalid input by dotted path: `params.storeId`, `query.limit`, `body.items.0.quantity`.

## Status codes

| Status | When |
| --- | --- |
| 400 | The body is not JSON, or the request is malformed |
| 401 | No credentials, or a key or token that is not valid |
| 403 | Your scopes, role or IP allowlist do not cover this |
| 404 | It does not exist, or belongs to another store |
| 409 | A conflict: a limit, a state that does not allow this, an edit conflict |
| 413 | The body is larger than 6 MB |
| 422 | Validation failed (see `fields`) |
| 429 | Rate limited; wait for `Retry-After` seconds |
| 5xx | Our side. Safe to retry with backoff for GET, and for writes that carry an idempotency key |

## In code

```ts
const res = await fetch(url, init);
if (!res.ok) {
  const problem = await res.json();
  const kind = problem.type.split('/').at(-1);
  if (kind === 'validation') showFieldErrors(problem.fields);
  else if (res.status === 429) await sleep(Number(res.headers.get('retry-after') ?? 1) * 1000);
  else throw new Error(problem.detail);
}
```
