Command Palette

Search for a command to run...

Hectal
PHASE 2Beginner ~21 min· topic 6 of 9

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.

Step 2: unknown statuses still get an answerdiagram
Rendering diagram…

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. 1

    Predict the table

    Before running, write down the "retry" column for each response, then run and check. Which one surprised you?

  2. 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. 3

    Handle Retry-After as a date

    Some servers send Retry-After as a date, not seconds. What would classify need to do when the header is not a whole number? Write the rule down before coding it.

Code & diagrams

Classify every status: who is at fault, and retry or not New tab

The responses are scripted. The lab below shows real headers. The 400 line uses the kind of error a bad temperature value produces.

Sign in to run this example in your browser.

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 teapot
Read the rate-limit headersreal model

Try: 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.

user

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.

terminal
$ Run
── what you'll see ──
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
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

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. 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. 2

    4xx: the request is the problem (mostly your bug). 400 bad request (a malformed or invalid value); 401 missing or wrong key; 403 the key is valid but not allowed; 404 wrong URL or a model name that does not exist; 413 the request is too large (often too many tokens); 422 valid shape but unacceptable values. Do not retry these. Fix the request, key, model name or input size. A bad value such as a temperature outside the allowed range, or an unknown model, is a typical 400 or 404.

  3. 3

    Two 4xx that are worth another try. 408 the request took too long to arrive, and 429 too many requests: you are over a rate limit (requests or tokens per minute, Topic 6.7). For 429 the response often includes a Retry-After header 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 no retry-after header). Read the error type or message before you retry. The lab below shows the rate-limit headers a real provider sends.

  4. 4

    5xx: the provider is the problem. 500 internal error, 502 bad gateway, 503 service unavailable or overloaded, 504 gateway timeout (Anthropic also uses 529 for 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. 5

    Read the error body, but carefully. Most APIs return JSON like {"error": {"message": "...", "type": "..."}}. The message is for humans and may change, so branch on the status (and a stable type or code where 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 a 502 with an HTML body.

  6. 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 200 can still hide trouble: finish_reason: length or an empty reply (Topic 2.3). Good code has three layers: transport errors, HTTP status errors, and reply-content problems.

Explain it without notes

01

Which status codes would you retry, and which would you never retry? Why?

02

How should code react to a 429?

03

Why not parse an error body as JSON without checking?

Practice

01

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.

02

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.