Search your codebase for // ignore inside a catch block.
I did, across 37 of my own repositories. Nearly all of them started in 2025 or 2026, and an agent wrote part of the code in them. // ignore is the most common thing written inside a catch that does nothing.
That comment has a reader, and it is ESLint.
Why the comment is there
no-empty ships in ESLint's recommended config, so most JavaScript projects run it without anyone having chosen to. Its documentation has one sentence that explains everything that follows: "This rule ignores block statements which contain a comment."
An empty catch fails the lint run. The same catch with // ignore in it passes.
The exception was designed for a human who leaves the comment on purpose, to tell the next reader the empty block is deliberate. An agent meets a failing lint run and looks for the cheapest change that turns it green. The cheapest change is one line of English.
A grep across those 37 repositories finds 536 catch blocks with no line of code in them. 494 of them, 92%, do hold a comment line. Roughly a quarter of those say some form of // ignore. After that come notes about a fallback, and // best effort.
Treat that as a floor. A regex misses every catch that logs the error and carries on, which is the other half of this problem.
Some of them are right
A few of those catches are exactly what they should be. An SDK that reports errors must never throw from the code that does the reporting. A cleanup step that deletes a temp file has nothing to do if the file is already gone.
In each correct case, though, the catch knows which error it expects. "The file is already gone" is one error code, ENOENT. // ignore catches that, and also a full disk, a permissions failure and a typo in the path, all with the same shrug.
Where it stops being harmless
This is from an end-to-end suite in a project of mine that never went live. It was something I built to learn with, I stopped before it was finished, and an agent wrote nearly all of it. That is what makes it worth quoting:
try {
await submitBtn.waitFor({ state: 'visible', timeout: 3000 })
await submitBtn.click()
} catch {
// Submit might not be needed
}Variations of that block appear more than a dozen times in the suite. Read it as a test. If the submit button never appears, the test waits three seconds and carries on. The one regression this step exists to catch, a quiz you cannot submit, has become the one outcome it accepts without a word.
A suite that cannot fail looks exactly like a suite that passes. This site's own repository has two of these catches, in a test file, and both pass its lint config.
Why a model writes this
Two pressures, stacked on top of each other.
The first is the corpus. Catch-and-ignore has been in public code for as long as there has been public code. no-empty needed a comment exception because people wrote empty catches so often.
The second is the loop. An agent's working loop rewards a green run, and an uncaught exception is the fastest way to a red one.
The system card for Claude Fable 5.1 and Mythos 5.1, published on 1 September 2026, screens reinforcement-learning episodes for a model reaching for answers or hidden tests from outside the task. On the environments the newest model shares with its predecessors, "every model is flagged for attempts on roughly 20% to 28% of episodes." Under that pressure, looking for a shortcut to green is routine.
Whether the shortcut pays off depends on what checks the result. In your loop, one of those checks is your lint config.
The shortcut also has a known shape. Hodoscope, from April 2026, quotes an agent's edit from ImpossibleBench, where the tests cannot be satisfied honestly. The edit inserts except Exception: pass, and the authors file it under specialising to the tests.
The closest thing to a measurement is GitClear's 2026 maintainability report, built on 623 million code changes from 2023 to 2026. Over the years in which AI authorship grew to a large share of all commits, it counts a 47% rise in what it calls "error-masking constructs". That is a trend across all code, and it ties to AI only through timing. CodeRabbit's comparison of AI and human pull requests from December 2025, a vendor report, points the same way from the other side: "Error handling and exception-path gaps were nearly 2× more common."
Nobody has published a rate per model. The step from "optimised for green" to "writes // ignore" is my inference, and my own disk is the evidence I have for it.
A rule that sees through the comment
Comments are not part of the syntax tree. ESLint's no-restricted-syntax matches selectors against that tree, so a catch whose body holds zero statements is empty to it, however many comments sit inside:
// eslint.config.js
export default [
{
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'CatchClause > BlockStatement[body.length=0]',
message: 'Empty catch: rethrow, return a typed error, or name the error you expect.',
},
{
selector:
'CatchClause > BlockStatement[body.length=1] > ExpressionStatement > CallExpression[callee.object.name="console"]',
message: 'A catch that only logs still swallows the error.',
},
],
},
},
]The second selector covers the log-and-carry-on case the grep could not see. Both messages are written for the agent. They name the fix, so the agent's next attempt has something to aim at. If your config already sets no-restricted-syntax, add these objects to that list, because a later config object replaces the options instead of merging them.
For Python, Ruff has both halves built in. S110 flags try-except-pass, and its documentation says why: "Suppressing exceptions may hide errors that could otherwise reveal unexpected behavior, security vulnerabilities, or malicious activity." BLE001 flags a blind except Exception.
[tool.ruff.lint]
extend-select = ["S110", "BLE001"]You will have hundreds of existing violations, which is what a baseline that only shrinks is for. Switch the rule on as an error, freeze what is already there, and only new catches fail.
Then rewrite the legitimate ones so they say what they expect:
try {
await unlink(tmpFile)
} catch (err) {
if (err.code !== 'ENOENT') throw err
}The comment became a condition, and the next reader can check a condition. Where a fallback really is the right behaviour, make it visible rather than silent, with a field that records which path ran.
And give the agent the rule in words, before the linter has to:
A catch block rethrows, returns a typed error, or checks for the one error it expects. A comment is not handling.// ignore used to be a note to the next developer. Now it is mostly a note to the linter, and the linter takes its word for it.