Errors, Retries & Degraded Modes

core25 min

In one line

Model APIs fail in ways ordinary APIs do not — the request can succeed while the answer is unusable — so "did it 200?" is not the question the UI should be asking.

What it is

Sort the failures by what the UI must do.

Transport failures are familiar: a 429 with a retry-after, a 5xx, an overloaded response, a dropped connection. Retry with exponential backoff and jitter, respect retry-after rather than guessing, and cap attempts. A 429 deserves an honest message about capacity, not a generic error.

Mid-stream disconnects are the awkward one. You have partial text on screen and the connection is gone. Keep the partial, mark it incomplete, and offer continue-or-retry. Silently discarding what the user was reading is the worst option.

Stop reasons are not errors but change the outcome. A response completing with max_tokens is truncated mid-sentence — offer to continue rather than pretending it finished. A refusal means safety classifiers declined; that needs a distinct, non-alarming message, and it is not something a retry of the same prompt will fix. And critically: check the stop reason before reading the content, because a refusal can arrive with an empty content array and code that indexes the first block will throw.

Tool failures belong to the loop, not the UI: return the error to the model as a tool result and let it adapt. Ending the conversation because one tool 500'd wastes the turn.

Content problems are the category with no HTTP status at all — valid JSON that fails your schema, a citation to a document that does not exist, an answer that is confidently wrong. Validate structured output against a schema at the boundary and treat a parse failure as a retryable condition, not a crash.

Then the degraded modes worth designing rather than discovering: fall back to a smaller or alternative model when the primary is unavailable, queue with an honest wait when rate-limited, and preserve the user's input on every failure path so nobody retypes a paragraph.

Why it matters

These products fail more often and more strangely than CRUD apps, and the error surface is a large share of the perceived quality. Reviewers of a take-home look for the failure paths specifically — the happy path is the easy half.

The empty-content-on-refusal detail is a genuine crash people ship.

Key points

  • Retry transport failures with backoff and jitter, honouring retry-after; give 429s a truthful capacity message.
  • On a mid-stream drop, keep the partial output, mark it incomplete, and offer continue or retry.
  • Check the stop reason before reading content — a refusal can carry an empty content array.
  • max_tokens means truncated, not finished; offer continuation.
  • Return tool errors to the model rather than ending the turn.
  • Validate structured output against a schema and treat parse failure as retryable.
  • Design the degraded modes — fallback model, honest queueing — and never lose the user's typed input.