Your AI-generated code isn’t working. Here’s how to find out why.

A practical method for debugging AI-generated code when prompting has stopped helping: reproduce it, read the seams, check the assumptions, and fix the cause.

By UmaizPublished 8 min read

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 reads user_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.log of an object in a browser shows a live reference, so an object mutated later looks wrong at the point you logged it. JSON.stringify freezes it.
  • If a value is undefined at a boundary, the interesting question is not “why is it undefined” but “what name was it sent under”. Log Object.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 null rather 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

  1. 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.
  2. Rewriting the file. You lose the working parts along with the broken one, and now you have a new set of untested assumptions.
  3. 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.
  4. 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.
  5. 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.

← All insights

Show me what’s stuck.

Don’t spend another six hours fighting the same bug. Send me what you’ve got — the messy version is usually the useful version.

Both open with a short template already filled in.