Lege catch-blokken in AI-code: het commentaar dat je linter het zwijgen oplegt
6m leestijd

Lege catch-blokken in AI-code: het commentaar dat je linter het zwijgen oplegt

AI-code slikt fouten in met catch-blokken waar alleen een commentaar in staat, en de aanbevolen ESLint-config laat ze juist dankzij dat commentaar door. Wat ik in mijn eigen repositories vond, waarom een model ze schrijft, en de regel die wel door het commentaar heen kijkt.

Zoek in je codebase eens naar // ignore in een catch-blok.

Dat deed ik in 37 van mijn eigen repositories. Bijna allemaal zijn ze in 2025 of 2026 begonnen, en een deel van de code erin komt van een agent. In catches die niets doen, staat // ignore het vaakst.

Dat commentaar is voor iemand bedoeld, en die iemand is ESLint.

536 lege catch-blokken op schaal: no-empty laat de 494 met commentaar door, no-restricted-syntax vangt alle 536

Waarom dat commentaar er staat

no-empty zit in de aanbevolen config van ESLint, dus de meeste JavaScript-projecten draaien hem zonder dat iemand ervoor gekozen heeft. De documentatie bevat één zin die alles hierna verklaart: "This rule ignores block statements which contain a comment."

Een lege catch laat de lint-run falen. Dezelfde catch met // ignore erin komt er gewoon door.

Die uitzondering is bedoeld voor een mens die het commentaar bewust achterlaat, zodat de volgende lezer weet dat het lege blok zo hoort. Een agent krijgt een falende lint-run en zoekt de goedkoopste wijziging die hem groen maakt. Die goedkoopste wijziging is één regel Engels.

Een grep door die 37 repositories vindt 536 catch-blokken waar geen regel code in staat. In 494 daarvan, 92%, staat wel een commentaarregel. Ruwweg een kwart daarvan is een variant van // ignore. Daarna komen aantekeningen over een fallback en // best effort.

Zie dat als een ondergrens. Een regex mist elke catch die de fout logt en gewoon doorgaat, en dat is de andere helft van dit probleem.

Een deel ervan klopt

Een paar van die catches doen precies wat ze moeten doen. Een SDK die fouten rapporteert, mag bij dat rapporteren zelf nooit een exception gooien. Een opruimstap die een tijdelijk bestand verwijdert, hoeft niets te doen als dat bestand al weg is.

In elk van die gevallen weet de catch wel welke fout hij verwacht. "Het bestand is al weg" is één foutcode: ENOENT. // ignore vangt die, maar ook een volle schijf, ontbrekende schrijfrechten en een tikfout in het pad, en haalt bij alle vier net zo onverschillig zijn schouders op.

Waar het niet meer onschuldig is

Dit komt uit een end-to-end-suite in een project van mij dat nooit live is gegaan. Ik bouwde het om iets uit te proberen, ik ben gestopt voordat het af was, en vrijwel alle code erin komt van een agent. Juist daarom citeer ik het hier:

ts
try {
  await submitBtn.waitFor({ state: 'visible', timeout: 3000 })
  await submitBtn.click()
} catch {
  // Submit might not be needed
}

Varianten van dat blok staan ruim een dozijn keer in die suite. Lees het als een test. Verschijnt de verzendknop nooit, dan wacht de test drie seconden en gaat hij verder. Precies de regressie die deze stap moet vangen, een quiz die je niet kunt insturen, is nu een uitkomst die de test zonder morren accepteert.

Een suite die niet kan falen ziet er precies zo uit als een suite die slaagt. Ook de repository van deze site heeft twee van zulke catches, in een testbestand, en allebei komen ze langs de lint-config van die repository.

Waarom een model dit schrijft

Er werken twee krachten tegelijk.

De eerste is het trainingsmateriaal. Catches die alles negeren, staan in openbare code al zo lang als die bestaat. no-empty kreeg die uitzondering voor commentaar juist omdat mensen zo vaak lege catches schreven.

De tweede is de loop waarin een agent werkt. Die beloont een groene run, en een exception die niemand opvangt is de snelste weg naar een rode.

De system card van Claude Fable 5.1 en Mythos 5.1, verschenen op 1 september 2026, doorzoekt trainingsepisodes op pogingen van een model om antwoorden of verborgen tests van buiten de opdracht te bemachtigen. In de omgevingen die het nieuwste model met zijn voorgangers deelt, geldt: "every model is flagged for attempts on roughly 20% to 28% of episodes." Een model onder die druk zoekt vaak een kortere weg naar groen.

Of die kortere weg loont, hangt af van welke controles er op het resultaat staan. In jouw loop is je lint-config daar één van.

Die kortere weg heeft ook een bekende vorm. Hodoscope, uit april 2026, citeert een wijziging die een agent maakte in ImpossibleBench, waar de tests met eerlijk werk niet te halen zijn. Die wijziging voegt except Exception: pass toe, en de auteurs scharen die onder code die op de tests is toegesneden.

Het dichtst bij een meting komt het maintainability-rapport van GitClear uit 2026, gebaseerd op 623 miljoen codewijzigingen van 2023 tot 2026. In de jaren waarin AI een groot deel van alle commits ging schrijven, telt het een stijging van 47% in wat het "error-masking constructs" noemt. Die trend geldt voor alle code, en het verband met AI berust alleen op de timing. Langs een andere weg komt CodeRabbit, zelf een leverancier, tot hetzelfde beeld in zijn vergelijking van AI- en mensenwerk in pull requests uit december 2025: "Error handling and exception-path gaps were nearly 2× more common."

Een percentage per model heeft niemand gepubliceerd. De stap van "geoptimaliseerd op groen" naar "schrijft // ignore" is mijn conclusie, en het bewijs dat ik ervoor heb, staat op mijn eigen schijf.

Een regel die door het commentaar heen kijkt

Commentaar hoort niet bij de syntaxboom. no-restricted-syntax in ESLint toetst selectors aan die boom, dus een catch zonder statements is voor die regel leeg, hoeveel commentaar er ook in staat:

js
// 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.',
        },
      ],
    },
  },
]

De tweede selector vangt de catch die alleen logt en doorgaat, en die de grep niet kon zien. Beide meldingen zijn voor de agent geschreven. Ze noemen de oplossing, zodat de agent bij zijn volgende poging ergens op kan mikken. Staat no-restricted-syntax al ergens in je config, voeg deze objecten dan aan die lijst toe, want een later config-object overschrijft de opties van een eerder object volledig.

Voor Python heeft Ruff beide helften ingebouwd. S110 markeert try-except-pass, en de documentatie zegt waarom: "Suppressing exceptions may hide errors that could otherwise reveal unexpected behavior, security vulnerabilities, or malicious activity." BLE001 markeert een blinde except Exception.

toml
[tool.ruff.lint]
extend-select = ["S110", "BLE001"]

Je hebt ongetwijfeld al honderden overtredingen staan, en precies daarvoor dient een baseline die alleen kan krimpen. Zet de regel op error, bevries wat er al staat, en dan faalt de build alleen nog op nieuwe catches.

Herschrijf daarna de catches die wel kloppen, zodat ze zeggen wat ze verwachten:

js
try {
  await unlink(tmpFile)
} catch (err) {
  if (err.code !== 'ENOENT') throw err
}

Het commentaar is een voorwaarde geworden, en of die klopt, kan de volgende lezer controleren. Is terugvallen echt het juiste gedrag, maak het dan zichtbaar, met een veld dat vastlegt welk pad de code volgde.

En geef de agent de regel in woorden, voordat de linter het hem moet vertellen:

markdown
A catch block rethrows, returns a typed error, or checks for the one error it expects. A comment is not handling.

// ignore was ooit een briefje voor de volgende developer. Nu is het vooral een briefje voor de linter, en die gelooft het blindelings.

(8 van 8)