What brokeUpdated 3 October 2026
Three errors that looked like empty results
In three places in the stock app, a failed call was turned into an empty but valid result. A news-tone job reported success with 0 rows, a US insider-data parser wrote 0/0/0 for every stock, and a volatility query quietly returned nothing. Here is how we found them and the rule that came out of it.
The short version
What went wrongIn three places a failed call became a valid empty result: a volatility query, a news-tone job reporting success with 0 rows, and insider data parsed to all zeros.
How we found itA guard for another finding, a manual verification round and a question from the project owner exposed them, not the app itself.
The rule nowKeep errors apart from empty results and throw when everything fails; a silent-zero sweep now flags all-zero columns unless registered with a reason and expiry date.
This post documents something Claude got wrong. The claim rests on a commit or a file, not on memory. All of Claude's mistakes
Between 31 July and 4 August 2026 we found the same bug three times in the stock app. A call failed, and the code turned the failure into a value that also meant “nothing here”. Nothing crashed and no job turned red. In the database, “there is no data” and “we could not get the data” looked exactly the same.
Henning writes no code himself, so all three bugs were in Claude’s code. For the volatility query, the commit says all six findings in that review were “in code I wrote the same day”.
1. A volatility query that never worked (31 July)
The project owner asked for more verification rounds before a planned Sunday deploy. One of the six findings was a database query for annualised volatility per instrument, which feeds a position-sizing mode. The commit says the query “never worked”. The database library expanded a list of IDs into the wrong type, and the query threw a type-cast error.
An error handler then caught that and returned an empty lookup table. The commit’s own words: “a completely legitimate state. Nothing failed.” The sizing engine saw “no volatility known” and quietly fell back to equal weighting.
What exposed it was a guard added for another finding in the same review. It makes the engine report when sizing falls back to equal weighting. That guard fired against real data. The fix replaced the silent fallback with logging. A smoke test was added that fails if the volatility source comes back empty. Afterwards it got values for 20 of 20 instruments, ranging from 13.9 % to 71.2 %.
2. A news-tone job that reported success with 0 rows (2 August)
In a later verification round we found that the macro news-tone data had not changed since 19 July, 344 hours earlier. The job behind it had reported success as recently as that night.
The run log showed the pattern. In 8 of 10 runs the job wrote 0 rows. The two runs that did write data, about 1,060 rows each, ran at times that mean they were started by hand. The scheduled runs always wrote nothing.
The root cause was checked against the real API. The news source (GDELT) rate-limits and answers with HTTP 429. A manual request got 429, then 429 again, then HTTP 200 with 51,900 bytes of valid data. In the commit’s words: “The source was never down. We gave up at the first rejection and called it success.”
The commit lists three layers that hid it:
- The fetch function returned an empty list both on a non-OK response and on any exception. “The call failed” and “this country has no data” became the same value.
- The job read an empty list as “nothing to do” and moved on.
- A total of 0 was logged as success.
The project’s own instructions already required retry with jitter on every external call. A file in the code even named GDELT as one of the sources still missing it. After the fix, the fetch throws on errors and uses the shared retry helper, which treats 429 as temporary. The job now tells failure apart from empty, and it throws if every country fails. Partial success is logged together with the countries that failed.
The same commit found a second silent gap. The map from country to the source’s country code used only full names (“Norway”, “Germany”). The instrument table stores two-letter codes (“NO”, “DE”). The only key that matched was “US”, because it happens to be both. The macro feature had been US-only, and 125 active instruments it was meant to cover fell outside it. Two-letter keys were added that point to the same codes as before. Countries with only a few instruments were left out on purpose, because a wrong code would give tone for the wrong country and still look valid.
After the fix, a real run hit 429, retried and fetched 3,177 points. The data went from 1 country to 3. Four countries still failed on the rate limit, and this time they were reported.
3. US insider data that was all zeros (4 August)
The project owner asked whether the insider-buying signal only covered Norwegian stocks. The answer was yes, though nobody had chosen that.
The insider table had 78 rows for US stocks (44 NYSE, 34 NASDAQ). Every one had net value 0, buy count 0 and sell count 0 over a 90-day window. The job had reported success ten runs in a row.
The cause: for Form 4 filings, SEC’s index points to a version rendered for humans. It has a .xml name, returns 200, is about 18 kB, and is HTML without a single transaction code in it. The raw XML sits in the same folder under a slightly different path. The parser found no transactions, the summary of an empty list was a set of perfectly legal zeros, and those zeros were saved as if they were a measurement.
The code already had a comment warning against all-null summaries overwriting good numbers, but only for an unexpected response shape. This response was well-formed. It was simply the wrong document. After the fix, the real SEC data contained actual sales transactions for US stocks.
Once real numbers flowed, a new risk appeared. One filing had a typo in the share price, a value seven digits long where a three-digit price was meant, which made a single transaction about −7.4 billion USD. Deploying the parser fix alone would have let that straight into the signal. The summary now checks each price against the last closing price and drops anything more than 10× above or below it. The number of dropped transactions goes to the log as a warning.
How they were found
None of the three was found by the app. A guard written for a different bug caught the volatility query. A manual verification round caught the news-tone job. A question from the project owner caught the insider data. The commit that followed says it plainly: a silent zero had sat there for weeks and was found because a human asked a question, which the commit calls the most expensive way to find something.
The rule that came out of it
Keep errors apart from empty results, and throw when everything fails. A source that is down should look down. An empty list has to mean “the source answered with nothing”, never “the call failed”.
To back the rule with a check, we added a silent-zero sweep to the app’s self-audit. It scans 15 decision tables for numeric columns that are all null, all zero or never vary. On its first run it found 10, the same 10 as a manual review, and all of them had an explanation.
Some zeros are correct. A window column is always 90, and a 90-day return can’t exist until the oldest signal is 90 days old. So every expected zero gets a written reason, and most get an expiry date. The 90-day return is excused until 21 September 2026. The day after an expiry date, the column counts as a finding again, without anyone touching the code. Eight tests cover that logic, including the one that matters most: on the day after expiry, the column is flagged again. By default every zero is flagged. A zero only stays quiet if it is registered with a reason.
Corrections
- Attribution corrected: an earlier version said the material did not show who wrote the code for two of the three bugs and did not put those mistakes on Claude. Henning writes no code himself; the code was written by Claude.