In the 2025 Stack Overflow developer survey, across 48,962 respondents, the single most cited frustration with AI tools was not that they produce garbage. It was AI solutions that are almost right, but not quite, named by 66 per cent. A further 45 per cent said debugging AI-generated code takes them longer than debugging their own.
In a survey that reads as a developer experience complaint. In a running business it is a data integrity problem, because almost right is the category of wrong that does not announce itself. A crash gets fixed on the day it happens. A function that catches an exception, returns an empty array, and lets the caller carry on gets fixed the month somebody notices the numbers do not add up.
Why agents write it this way
A model is optimising for code that looks finished and runs without complaint. An unhandled error path is visibly incomplete, so it gets handled. Handling it correctly requires knowing what the business wants to happen when a charge half-succeeds, and nothing in the prompt says. So the agent picks the option that keeps the program running, because that is the one that demos.
This is not a criticism of the tool and it is not solved by a better prompt. It is the same gap as a permissive database policy: a generator that has to guess at your rules will guess in the direction that makes the screen work.
Two greps, ten minutes
Run these at the root of your repository. Neither is exhaustive and both have false positives, which is the point: you are building a list to read, not a verdict.
# 1. Catch blocks that do nothing at all.
grep -rn --include=*.ts --include=*.tsx \
-E "catch *(\([^)]*\))? *\{ *\}" src/
# 2. Failures quietly converted into a value.
grep -rn --include=*.ts --include=*.tsx \
-E "\.catch\(\(\) => (null|undefined|\[\]|false|0)\)" src/Then read each hit and ask one question: if this fails in production at three in the morning, how does anybody find out? If the honest answer is that a customer emails you, it belongs on the list. A clean result here is genuinely clean, and plenty of repositories come back empty on both.
What to do with the list
- Sort it by what the code touches. Money, permissions and anything that writes customer data go first. A swallowed error in a UI animation can stay swallowed.
- For each one in that top group, decide what should happen on failure and write it down in a sentence before you write code. Retry, refuse, or record and alert. Most of these bugs exist because that sentence was never said.
- Make the failure loud at the boundary: raise, return an error the caller must handle, and let it reach error tracking rather than a console.
- Add one test per fixed path that asserts the failure behaves as decided. This is the part that survives the next regeneration.
- Only then widen to the rest of the list.
The reason to do this as a pass rather than as you notice them is that these bugs are invisible individually and obvious in aggregate. Twenty swallowed errors across a codebase is not twenty small problems. It is one large statement about whether the application can be trusted to tell you when it is broken.
The part that is not about errors
Every item above is the same move: make the system tell you the truth about itself. That is also what monitoring is, what a tested restore is, and what a database role with real grants is. An agent writing the code changes how fast the gaps appear, not what closes them.
It is the first pass we run on a Cursor project going to production, before anything about infrastructure, because a deployment pipeline on top of code that cannot report its own failures just ships the silence faster.
Written by Kato Chojiro
Takes AI-built MVPs to production: security, data, and the infrastructure underneath.
Connect on LinkedIn