Topic 2.6
Errors and Status Codes
In one line
When a call fails, the HTTP status says whose problem it is: 4xx usually means your request needs fixing, 429 means slow down, 5xx means the provider is struggling. Classify every error so code retries only what can succeed and fails loudly on the rest.
Think of it like this
A returned parcel with a reason sticker. "Wrong address" means you must fix the label before sending again. "Recipient not home, will redeliver" means just try again tomorrow. "Depot flooded" means wait. Sending the same wrongly addressed parcel ten times will not help, and giving up on a redeliverable one loses a sale. The sticker (the status code) tells you which.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Status code
- The three-digit number in a response that says how the request went.
- 4xx
- Status codes 400 to 499: the request has a problem. Mostly not worth retrying.
- 5xx
- Status codes 500 to 599: the server has a problem. Usually temporary.
- 429 Too Many Requests
- You went over a rate limit and must wait.
- Retry-After
- A response header giving how many seconds to wait before trying again.
- Retryable error
- An error that a later attempt may fix: 408, 429, 5xx and network failures.
- Classification
- Putting each error into a category that decides what the code does next.
Step by step
01Step 1: one table decides everything
The example keeps a small table: status, whose fault, what to do, and whether to retry. Having it in one place means changing a policy changes one line, and tests can check the table.
02Step 2: unknown statuses still get an answer
classify falls back to a rule for any 5xx and to "log it and fail" for anything else (the example sends a 418). Never leave a path where an unlisted status causes an exception inside the error handler.
03Step 3: read the wait time
For 429 the example prints the retry-after header value. A client that ignores it and retries at once makes the overload worse and can get the key blocked longer.
04Step 4: survive a body that is not JSON
The 502 response has an HTML body. error_message catches the parse failure and returns the first characters of the text instead. Bugs in error handlers are the worst bugs, because they hide the original error.
05Step 5: see real rate-limit headers
Open the lab below, press Send and look at the response headers in Raw HTTP. Providers report limits and remaining quota on every response, not only on errors. Read the names and values; Topic 6.7 uses them to avoid hitting 429 at all.
06Step 6: turn the table into types
In real code, turn the table into exception types or an error object with retryable and retry_after fields. The production client in Topic 2.9 does this. Callers then write if error.retryable instead of memorising status numbers.
Try it yourself
- 1
Predict the table
Before running, write down the "retry" column for each response, then run and check. Which one surprised you?
- 2
Add a status
Add
409(conflict) to the table as "you" / "do not retry". Add a response with that status to the list. Which line of output proves it works? - 3
Handle Retry-After as a date
Some servers send
Retry-Afteras a date, not seconds. What wouldclassifyneed to do when the header is not a whole number? Write the rule down before coding it.
Code & diagrams
The responses are scripted. The lab below shows real headers. The 400 line uses the kind of error a bad temperature value produces.
Expected output
400 you retry=no wait=- | fix the request | temperature must be between 0 and 2
401 you retry=no wait=- | check the API key | Invalid API key
404 you retry=no wait=- | check the model name and URL | The model does not exist
429 limit retry=yes wait=7s | wait, then retry | Rate limit reached for requests
503 provider retry=yes wait=- | retry with backoff | The server is overloaded
502 provider retry=yes wait=- | retry with backoff | (no JSON error) <html>502 Bad Gatewa
418 unknown retry=no wait=- | log it and fail | I am a teapotTry: Press Send and open Raw HTTP. Find the headers whose names contain ratelimit and note the limit and remaining values. Send again: which value changed? You will not see a 429 unless you exceed the limit, so do not try.
Goals
- The reply finished (finish_reason is stop)
- The reply contains the word ready
Sign in to send this request to a real model (free).
Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Assume every error body is JSON
In the Python example change error_message to just return json.loads(body_text)["error"]["message"] (remove the try/except), then run it.
Myth vs fact
Myth
Retry every error a few times and you are safe.
Fact
Retrying 400, 401 or 404 can never succeed and wastes time and quota. Retry only errors that can change on their own.
Myth
A 5xx is always the provider's fault and nothing for me to do.
Fact
Your code decides how to cope: retry, fall back, queue or show a calm message. Unhandled, it becomes your outage.
Myth
The error message text is a stable contract.
Fact
Messages change. Branch on the status (and a documented error type or code), and log the message.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Keep per-status counters in metrics (Topic 11.4). A burst of 401s means a rotated or leaked key; of 429s, a limit to raise or a loop to fix; of 5xx, a provider incident.
- ▸
A 4xx caused by oversized input (413, or a 400 about context length) is a product decision: truncate, summarise or refuse. Do not retry it blindly.
- ▸
Make retries safe: model calls are mostly read-only, but a retried call that triggers a tool with side effects (Phase 7) can act twice. Use idempotency keys (a unique id per action that lets the receiver ignore a repeat) or keep side effects out of the retried part.
- ▸
Retried calls still cost money. A request that timed out on your side may have been fully processed and billed by the provider. Count retries in your cost metrics, and keep the attempt cap low (Topic 6.8).
Remember this
- 1
Why this exists. Calls fail for different reasons that need opposite reactions. A typo in your request fails identically forever; a busy server succeeds a second later. If your code treats every failure the same, it either gives up too early or retries bugs for ever, spending time and money. The first step of good error handling is classification: decide who is at fault and whether another attempt can help.
- 2
4xx: the request is the problem (mostly your bug).
400bad request (a malformed or invalid value);401missing or wrong key;403the key is valid but not allowed;404wrong URL or a model name that does not exist;413the request is too large (often too many tokens);422valid shape but unacceptable values. Do not retry these. Fix the request, key, model name or input size. A bad value such as atemperatureoutside the allowed range, or an unknown model, is a typical400or404. - 3
Two 4xx that are worth another try.
408the request took too long to arrive, and429too many requests: you are over a rate limit (requests or tokens per minute, Topic 6.7). For429the response often includes aRetry-Afterheader with the seconds to wait; honour it, then retry with backoff (Topic 6.8). Careful: not every 429 is a rate limit. When you run out of credit or hit a spend limit, some providers also answer 429, and waiting will not fix it (OpenAI's error guide says so, and Anthropic's spend-cap 429 has noretry-afterheader). Read the errortypeor message before you retry. The lab below shows the rate-limit headers a real provider sends. - 4
5xx: the provider is the problem.
500internal error,502bad gateway,503service unavailable or overloaded,504gateway timeout (Anthropic also uses529for overloaded). These are usually temporary, so retry with exponential backoff and jitter, with a limit on attempts, and consider a fallback model or provider when they persist (Topic 6.9). - 5
Read the error body, but carefully. Most APIs return JSON like
{"error": {"message": "...", "type": "..."}}. Themessageis for humans and may change, so branch on the status (and a stabletypeorcodewhere one exists), and log the message. Gateways and CDNs in front of the API can answer with an HTML page or plain text, so parsing must not assume JSON. The example handles a502with an HTML body. - 6
Beyond HTTP status. Network failures produce no status at all: DNS errors, connection reset, TLS failure and timeouts raise exceptions in your client library. Treat those as retryable too (with limits). And a
200can still hide trouble:finish_reason: lengthor an empty reply (Topic 2.3). Good code has three layers: transport errors, HTTP status errors, and reply-content problems.
Explain it without notes
Which status codes would you retry, and which would you never retry? Why?
How should code react to a 429?
Why not parse an error body as JSON without checking?
Practice
Write is_retryable(status) returning True for 408, 429 and 500 to 599, and False otherwise. Test it on 200, 400, 401, 429 and 503.
Write wait_seconds(headers, attempt) that returns the integer in the retry-after header when present, otherwise 2 ** attempt (attempt starts at 0).
Trade-offs
- ↔
Aggressive retries recover more calls but add latency, load and cost; conservative ones fail faster and loudly.
- ↔
A central classification table is easy to test; provider-specific error types give more detail but need more code.
Done when you can
Done when you can say which status codes are your bug, which are limits and which are provider trouble.
Done when you never retry a 4xx other than 408 and 429.
Done when your error parsing survives a non-JSON body.
Done when you can read a rate-limit header from a real response.