← All notes

22 Jul 2026

The Claude API gotcha that gave me silent nulls

I've been chipping away at a football stats and predictions side project, and part of it generates a short written summary from match data using Claude. Nothing complicated, a handful of numbers in, a sentence or two out. Except every so often the call would just come back empty. No exception, no error message, nothing, in a code path that was meant to always produce text.

Turned out max_tokens is a shared budget across thinking and the actual output, not two separate pots like I'd assumed. I had adaptive thinking switched on with a fairly tight max_tokens (400 at the time), thinking of it as extra reasoning layered on top of the response. It isn't. If reasoning burns through the whole budget before the model even gets to writing an answer, you get stop_reason: max_tokens back with no text block at all. My code just returned null in that case, which looks exactly like a legitimate refusal or an empty result, not a request that ran out of room.

Fixed it in two parts. Mostly just giving the call proper headroom, somewhere around 1500 to 2000 tokens depending on how much reasoning the task actually needs, so thinking has space to finish before it starts writing. And I stopped swallowing the failure silently:

$response = $client->messages->create(...);

if (! $response->hasTextBlock()) {
    Log::warning('Claude returned no text block', [
        'stop_reason' => $response->stopReason,
    ]);

    return null;
}

At least now, if it happens again, it says so.

// I use cookies to understand how visitors use this site. I'll only set analytics cookies if you accept.