When AI-generated code doesn’t work, the fastest way forward is almost never another prompt. It’s to reproduce the failure reliably, then read the places where two separately-generated pieces of code meet. That seam is where the bug lives the overwhelming majority of the time — not inside any single function, which the model probably wrote correctly.
This matters because the prompt-fix loop has a specific failure mode. Each round the model sees your description of the symptom, not the system. It writes a locally-plausible change, the symptom moves, and you describe the new symptom. Three rounds in, you have four modifications that individually look fine and collectively don’t cohere.
Why generated code fails at the seams
An LLM writes each piece of code against the context it has at that moment. Ask for a signup form and you get a good signup form. Ask for an API route later and you get a good API route. Nobody — not you, not the model — checked that the field the form sends is the field the route reads.
The result is code that is locally correct and globally inconsistent. Some recurring shapes:
- The frontend sends
userId, the backend readsuser_id. - One file assumes the API returns
{ data: [...] }, another assumes a bare array. - Two components each keep their own copy of the same state, and they drift.
- An error is caught and logged in a way that makes the real failure invisible.
- A date is stored as a local-time string in one place and parsed as UTC in another.
None of these are visible in a single file. All of them are obvious the moment you look at both sides at once.
Step 1: Make it happen on demand
A bug you can’t reproduce is a bug you can’t confirm you fixed. Before changing anything, get the failure to occur reliably.
Write down the exact sequence: which page, which input, which account, which browser. If it only fails sometimes, look for what differs between the runs — an empty list, a slow response, a second click, a logged-out session, a different timezone.
This step alone often finds the cause. “It only breaks for users with no orders” is most of a diagnosis.
Step 2: Find where the data actually changes
Rather than reading the code from the top, follow one piece of data from where it enters to where it goes wrong. Log at each boundary:
// At every hop, log the shape — not just the value.
console.log('[submit] payload', JSON.stringify(payload))
console.log('[api] received', JSON.stringify(req.body))
console.log('[db] writing', JSON.stringify(record))
Then compare the outputs. The hop where the data stops looking the way the next piece expects is your bug. This takes a few minutes and replaces an hour of guessing.
Two things worth knowing here:
console.logof an object in a browser shows a live reference, so an object mutated later looks wrong at the point you logged it.JSON.stringifyfreezes it.- If a value is
undefinedat a boundary, the interesting question is not “why is it undefined” but “what name was it sent under”. LogObject.keys()on both sides.
Step 3: Check the assumptions, not the syntax
Generated code is syntactically excellent and assumption-heavy. Ask, at each boundary:
- What happens if this array is empty?
- What happens if this request fails, or takes eight seconds?
- What happens if the user is logged out, or their token just expired?
- What happens if this runs twice, because someone double-clicked?
- What happens if this field is
nullrather than missing?
Prototypes are written for the happy path. Real users find the others immediately.
Step 4: Fix the cause
The tempting fix is the one that makes the error stop appearing:
// Makes the symptom vanish and the bug permanent.
try {
await saveOrder(order)
} catch (e) {
// ignore
}
That is worse than the original bug, because now it fails silently. If you catch something, either handle it meaningfully or re-throw it:
try {
await saveOrder(order)
} catch (error) {
logger.error('order save failed', { orderId: order.id, error })
throw error // let the caller decide what the user sees
}
Mistakes to avoid
- Prompting again after two failed attempts. If two rounds haven’t fixed it, the model doesn’t have the context it needs. More rounds won’t add it.
- Rewriting the file. You lose the working parts along with the broken one, and now you have a new set of untested assumptions.
- Changing several things at once. When it starts working you won’t know which change did it, and you’ll carry the other three forever.
- Trusting a fix you can’t demonstrate. Re-run the exact reproduction from step 1. “It seems fine now” is how bugs come back next week.
- Deleting the logging. Leave the boundary logs in behind a debug flag. You will want them again.
A checklist you can work through
- I can reproduce the failure on demand, with a written sequence
- I know the exact boundary where the data stops being correct
- I’ve compared both sides of that boundary — names, shapes, types
- I’ve checked empty, null, slow, failed and repeated cases
- My fix addresses the cause, not the visible symptom
- The original reproduction now passes
- I’ve checked the obvious neighbours for the same mistake
When to stop and get someone to look
Some honest signals that the loop won’t close on its own: the bug only happens in production, the failure moves each time you touch it, the fix requires understanding code nobody on your team has read, or it involves auth, payments or data integrity — where a wrong guess costs more than the delay.