Why Your LLM Returns an Empty Response: Three Causes That Look Identical

By Promptster Team · 2026-08-16

Your request returns HTTP 200. Tokens are billed. The response body is empty, or your code prints a fallback string, or the user sees nothing.

There is no error to search for. This is the most confusing failure mode in modern LLM APIs, and it got substantially more common in 2026 because reasoning models changed the shape of a response.

We hit all three causes in production inside a single month. They're indistinguishable from the outside and have completely different fixes. Here's how to tell them apart.


Cause 1: The answer is there, behind a reasoning block

Symptom: Empty string where the text should be. Output tokens billed in the hundreds.

Reasoning models return content as an ordered list of blocks. On Claude 5-family models the shape is:

content: [
  { type: "thinking", thinking: "" },   ← block 0
  { type: "text",     text: "**Wednesday**" }   ← the answer
]

The thinking block comes first, and its text is empty by default (display defaults to "omitted" — the reasoning happens and is billed, but isn't returned).

So this, which was correct for years:

const text = response.content[0].text;   // undefined

...silently yields nothing on every prompt where reasoning fires.

The tell: output tokens are high, stop_reason is end_turn, and the response looks successful. Log the block types — if you see ['thinking', 'text'], this is you.

The fix — join every text block rather than reading the first:

const text = (response.content ?? [])
  .filter(b => b.type === 'text')
  .map(b => b.text ?? '')
  .join('');

Why it hides: it only fires when reasoning fires. Our smoke tests used "Reply with exactly: OK" — adaptive thinking never triggers on a trivial prompt, so every test passed while every hard prompt returned nothing.


Cause 2: A safety refusal with an empty content array

Symptom: HTTP 200, content: [], one output token billed.

Safety classifiers can decline a request. That is not an HTTP error:

stop_reason: "refusal"
stop_details: { type: "refusal", category: "cyber", explanation: "..." }
content: []

Your retry logic won't catch it — there's nothing to retry, and the status code is 200.

We hit this asking Claude Opus 5 to review a Python function that contained a SQL injection. It declined under category cyberthree times out of three, on a routine code-review prompt.

The tell: stop_reason === "refusal", empty content, exactly 1 output token.

The fix — check stop_reason before reading content:

if (response.stop_reason === 'refusal') {
  const category = response.stop_details?.category;
  // surface it, rephrase, or fall back to another model
}

Anthropic also supports a server-side fallbacks parameter that reruns a refused request on a different model automatically. If refusals would break your product, that's the durable fix.

One warning from our testing: refusals are not predictable from the prompt alone. On identical vulnerable code, "Explain what this Python function does" was refused 5/5 while "Is this Python function secure?" was refused 0/5. The security-framed prompt passed; the neutral one didn't. You cannot reason your way to which prompts are safe — you have to handle the case.


Cause 3: Reasoning consumed the entire token budget

Symptom: stop_reason: "max_tokens", output tokens exactly equal to your limit, no text block at all.

max_tokens caps thinking plus the answer. On a hard prompt, adaptive thinking can spend the whole allowance before the model writes a word.

We reproduced this against Claude Opus 5 with a deliberately hard logic problem:

max_tokens stop_reason Blocks returned Thinking tokens Answer
300 max_tokens ['thinking'] 299 empty
800 max_tokens ['thinking'] 800 empty
2000 max_tokens ['thinking'] 2000 empty

At max_tokens: 2000 — a perfectly reasonable default — the model returned nothing and billed for 2,000 tokens. There is no text block in the response at all, so even a correct parser has nothing to extract.

The tell: stop_reason === "max_tokens", output_tokens equals your cap exactly, and content has no text block.

The fix: raise max_tokens substantially for reasoning models — treat it as a budget for reasoning and output, not output alone. If cost matters more than depth, lower the effort setting instead of the token cap, or disable thinking where the model permits it.


A 30-second diagnostic

Log these four fields on any empty response and the cause is immediate:

console.log({
  stop_reason: r.stop_reason,
  blocks: (r.content ?? []).map(b => b.type),
  output_tokens: r.usage?.output_tokens,
  thinking_tokens: r.usage?.output_tokens_details?.thinking_tokens,
});
stop_reason Blocks Cause
end_turn ['thinking', 'text'] 1 — parser skipping the answer
refusal [] 2 — safety refusal
max_tokens ['thinking'] 3 — budget eaten by reasoning

The pattern underneath

All three are the same underlying change: reasoning models altered the shape of a successful response, and integrations written against the old shape fail silently rather than loudly.

That's the dangerous part. A 500 gets noticed in an hour. An empty string that costs money and returns HTTP 200 can run for weeks — ours did, and it took a benchmark producing an impossible result for us to catch it.

If you're integrating a reasoning model, test with a prompt hard enough to make it think. A smoke test that asks for 2 + 2 will pass no matter how broken your parser is.

Try it yourself

To compare how different models shape their responses to the same prompt, that's Promptster.


All three causes reproduced against the Anthropic API on 2026-08-09 using claude-opus-5. Cause 1 and cause 2 were production bugs in our own integration, both since fixed. The cause-3 table is a direct measurement, three token limits, same prompt. The specific block shapes described here are Anthropic's; OpenAI and Google reasoning models return different structures but fail the same way — a response shape your parser wasn't written for.