# Errors and retries

Every error is [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem+json.

```json
{
  "type": "https://devs.inittasks.com/errors/validation_failed",
  "title": "Validation failed",
  "status": 422,
  "detail": "a sentence for a developer; never branch on it",
  "instance": "/todos/8E4F2A1B",
  "request_id": "3f9a1c7e-2b44-4d1e-9c30-5a7e8b2d1f60",
  "errors": [{ "pointer": "/title", "message": "must be at most 512 characters" }]
}
```

Branch on `type`. Never branch on `detail`, and never on the status alone, because several codes share a status. Every `type` is a live page in the [error catalogue](/errors).

Keep `request_id` if you report a problem. It is the only handle that ties your call to the server log, and the log holds no request bodies.

## Which errors are worth retrying

| you get | retry | why |
|---|---|---|
| [rate_limited](/errors/rate_limited) (429) | yes, after `Retry-After` | a bucket refills |
| [internal](/errors/internal) (500) | yes, with backoff | the server failed, your request did not |
| [validation_failed](/errors/validation_failed) (422) | no | it will fail identically forever |
| [unauthorized](/errors/unauthorized) (401) | no | refresh the token first, then send it again |
| [grant_revoked](/errors/grant_revoked) (401) | no | the grant is gone. Send the user through approval |
| [conflict](/errors/conflict) (409) | no | a reused key with a different body is a bug, not a blip |

## Idempotency keys

A write that times out may still have succeeded. Retrying it without a key creates a second row.

Send `Idempotency-Key` on every write. The same key with the same body replays the original response for 24 hours. The same key with a different body is a [conflict](/errors/conflict), because it means two different operations were given the same name.

Generate the key once per logical operation, outside your retry loop.

```bash
#!/usr/bin/env bash
# One key per logical operation, generated ONCE and reused by every retry.
KEY=$(uuidgen)

for attempt in 1 2 3 4 5; do
  response=$(curl -s -w '\n%{http_code}' https://api.inittasks.com/v1/todos \
    -X POST \
    -H "Authorization: Bearer $INITTASKS_TOKEN" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: $KEY" \
    -d '{"title":"buy milk"}')

  status=$(printf '%s' "$response" | tail -n1)
  body=$(printf '%s' "$response" | sed '$d')

  case "$status" in
    2*) printf '%s\n' "$body"; exit 0 ;;
    429|5*)
      # Back off. Retry-After is on a 429; this doubles from one second.
      sleep $(( 2 ** (attempt - 1) ))
      ;;
    *) printf 'giving up: %s\n' "$body" >&2; exit 1 ;;
  esac
done
```

A key generated inside the loop defeats the whole mechanism, which is the bug this example exists to prevent.

## Backing off

On a 429, sleep for `Retry-After` seconds. It is a number the server gives you, not a guess.

On a 5xx, double the wait each attempt and add jitter. Without jitter, a server restart gathers every client into one retry at the same instant.

Give up after five attempts and surface the failure. A write that has failed five times is not going to succeed on the sixth, and a queue that never drains is harder to debug than an error.

## Creating with your own id

You can supply `id` on a create. A second create with the same id returns [duplicate_id](/errors/duplicate_id), which usually means your first write landed. Fetch the row and carry on.

This is a second way to make a create safe to repeat, and it survives longer than the 24-hour idempotency window.

## Next

- [Rate limits](/rate-limits) for the buckets and the headers.
- [The error catalogue](/errors) for one page per code.
