How to Debug AI-Generated Code: A Step-by-Step Process

You didn't write the code that just broke, so the usual debugging instincts don't apply. Here's the actual process: localize the bug before touching anything, make the AI explain the suspect function line by line, add targeted logging instead of guessing, and test one hypothesis at a time, walked through on a real bug.

Steve Jefferson
Steve Jefferson
Developer Advocate
3 August 20261 min read

Something is broken. You are looking at a stack trace, or a blank screen, or a number that should be 12 and is instead 47. You did not write this code. An agent did, three prompts ago, and you have no mental model of it to fall back on.

This is the actual problem with debugging AI-generated code: not that the bugs are exotic, but that you're starting from zero context every time. Here's a process that works whether the code came from Claude, Cursor, Copilot, or whatever agent is fashionable this quarter: localize before you touch anything, use the AI to explain rather than just fix, instrument the code instead of guessing, and change one variable at a time. Below is the full workflow plus a worked example of chasing a real bug through all four steps.

Why the usual debugging reflexes don't transfer

When you debug your own code, you already have a hypothesis before you open the file. You remember writing the loop, you remember the edge case you skipped, you remember thinking "I'll fix that later." That memory is doing half the diagnostic work.

None of that exists with AI-generated code. You're reading a function for the first time at the exact moment it's failing, under time pressure, with no idea which of its assumptions are load-bearing. Standard advice like "add a print statement" or "step through it in the debugger" still works, but only if you apply it in an order that compensates for the missing context. That's what this process does. It's not about why the AI wrote a bug in the first place, that's a separate problem, or how to catch issues before you ship, also separate. This is about what to do once something is visibly broken in front of you.

Step 1: Localize the bug before you touch anything

The single biggest time sink in debugging unfamiliar code is fixing the wrong thing because you started editing before you knew where the problem actually lived. Resist the urge to change code until you can point at a specific function and say "the bug is in here."

Bisect the change history if you don't know when it broke

If the bug is a regression, it used to work and now it doesn't, and you've got more than a couple of commits between "working" and "broken," don't scroll through diffs by eye. Use `git bisect`, which does a binary search over your commit history:

bash
git bisect start
git bisect bad                # current commit is broken
git bisect good v1.4.0        # this tag/commit was known-good
# git checks out a commit halfway between the two
# test it, then tell git the result:
git bisect good   # or: git bisect bad
# repeat until git names the exact commit
git bisect reset

This is unusually well-suited to AI-generated codebases specifically because coding agents tend to produce many small, frequent commits, one per prompt or per accepted diff, which gives bisect a lot of granular history to search through instead of one giant unreviewable commit. Ten commits takes about four tests; a thousand takes about ten. If your tests are scriptable, automate the "good/bad" check with git bisect run <script> and let it finish unattended.

Narrow it to one function

Once you have the commit, or even without one, if it's not a regression, diff or read only the code that changed or the code on the direct path to the failure. Don't read the whole file. Ask yourself: what's the smallest unit of code that, if I stubbed it out, would make the failure disappear or change shape? That's your suspect.

Step 2: Make the AI explain the suspect code back to you, line by line

Once you've localized to a function or a small block, resist the temptation to immediately ask "fix this." You'll get a plausible-looking patch that may address the symptom without touching the cause, and you still won't understand what happened.

Instead, ask the AI to narrate the code, not fix it:

Explain this function line by line. For each line, state what
you believe is true about the inputs and state at that point,
and flag any line where that assumption could be wrong.

This does two things. First, it forces the model to make its assumptions explicit, which is often where the bug is hiding: a variable it assumes is defined, a list it assumes is non-empty, an async call it assumes has resolved. Second, it gives you, the reader, a fast way to build the mental model you're missing, without reading every line yourself at the same depth.

Feed it the actual error and stack trace, not a paraphrase. "It's not working" gets you nothing. A full traceback, the exact input that triggered it, and the exact output you got versus expected gets you a model that can reason instead of guess.

Step 3: Instrument before you guess

If the explanation doesn't reveal the bug, and it often doesn't on the first pass, the next move is logging, not more staring. But log with intent. Don't sprinkle console.log everywhere; put logging at the boundaries of the suspect function, the values coming in and the values going out.

Ask the AI directly:

Add temporary logging at the input and output of this function,
and at any point where state is read or written. Label each log
so I can tell them apart in the output. Don't change any logic.

Run it once, look at what actually happened versus what you expected, and remove the logging afterward. This is the fastest way to find the exact line where reality diverges from the code's assumptions, and it's more reliable than asking the model to "find the bug" from a cold read of the source, since a model reading its own code in isolation has the same blind spots it had when writing it.

Step 4: Test one hypothesis at a time

This is the part people skip under pressure, and it's the part that actually matters. Write down, in one sentence, what you think is wrong. Then predict what you'd see if you're right. Then make the smallest possible change or check to test that specific prediction. Then look at the result before touching anything else.

The failure mode to avoid: asking an agent to "try fixing it" and letting it change five things at once. If the bug goes away, you don't know which change fixed it, and you've likely introduced two new behaviors you haven't tested. If it doesn't go away, you've burned a turn and muddied the diff. One hypothesis, one change, one observation, every time.

A Lightrun survey of senior engineering leaders cited by VentureBeat found that 43% of AI-generated code changes needed manual debugging in production even after passing QA and staging, and that not one of the surveyed organizations could verify an AI-suggested fix in a single redeploy cycle. That's what happens when "try fixing it" replaces "test one hypothesis": you burn cycle after cycle chasing a fix that half-worked.

Worked example: a stale value that only shows up under load

Here's the whole process end to end, on a bug class that shows up constantly in AI-generated frontend code: a counter that undercounts when clicked quickly.

The symptom. A button is supposed to increment a counter after a 3-second delay. Click it three times fast. Expected: count goes to 3. Actual: count goes to 1.

Localize. There's no backend involved and no regression to bisect here, this shipped broken from the start. So we go straight to narrowing: is it the click handler, the timer, or the render? A quick log at render time shows the component re-renders correctly on state changes, so the bug is inside the handler or the timeout, not the rendering.

The suspect function, generated by an agent a few prompts earlier, looks like this:

jsx
function handleClick() {
  setTimeout(() => {
    setCount(count + 1);
  }, 3000);
}

Explain it line by line. Asking the AI to narrate this function surfaces the load-bearing assumption immediately: it states that count inside the setTimeout callback is "the current value of count at the time the timeout fires." That assumption is wrong, and naming it out loud is what makes the bug visible. In JavaScript, the callback closes over the value of count from the render when handleClick was called, not the value at the time it fires.

Form the hypothesis. All three clicks capture the same stale count, say 0, so all three timeouts eventually run setCount(0 + 1), and the last one to execute wins. Prediction: if this is right, logging the captured value at click time and at fire time should show all three logging the identical number.

Instrument and test.

jsx
function handleClick() {
  const capturedCount = count;
  console.log('captured at click:', capturedCount);
  setTimeout(() => {
    console.log('using at fire:', capturedCount);
    setCount(capturedCount + 1);
  }, 3000);
}

Run it, click three times fast. The logs show captured at click: 0 three times in a row. Hypothesis confirmed. This isn't a race condition or a rendering bug, it's a classic stale closure.

Fix and verify. The fix is to stop reading the captured variable and use the functional update form, which always receives the true current state at the moment it runs:

jsx
function handleClick() {
  setTimeout(() => {
    setCount(c => c + 1);
  }, 3000);
}

Click three times fast again: count goes to 3. Remove the temporary logging. Done, and you now understand exactly why it broke, which matters because stale closures show up again anywhere an agent writes a callback, an event handler, or a timeout that references outside state.

When to stop debugging and just regenerate

Sometimes the cheaper move is to throw the function away rather than patch it, but only after you've done step 1. Once you know exactly which function is broken and why, you can hand the AI a tight, specific spec: this function needs to do X, here's the failing input, here's the expected output, here's why the current version fails, and let it regenerate from a clean slate. Regenerating before you've localized the problem just gives you a new implementation with the same blind spots, since the model still doesn't know what actually went wrong.

Debugging AI-generated code is slower than debugging your own code for a while, because you're building context you never had. Following a fixed process, localize, explain, instrument, test one hypothesis, is what makes that gap close instead of costing you an afternoon every time.

If you want the upstream half of this problem, the pillar guide on AI coding tools covers the landscape of agents and editors, why AI writes code that doesn't work explains the root causes this post deliberately skips, and how to review AI-generated code before you ship it covers catching issues before they reach this stage at all. If you're relying on an autonomous agent rather than a single-shot assistant, what is an AI coding agent is worth reading alongside this, and pairing this process with how to use AI to write tests turns each confirmed hypothesis into a regression test so the same bug class doesn't come back.

Frequently asked questions

How do you debug code an AI wrote when you can't read it fluently?

Don't start by reading the whole file. Localize first (bisect the commit history or narrow to the function on the failure path), then ask the AI to explain that specific function line by line, stating its assumptions about the inputs at each step. That narration builds the mental model you're missing faster than reading the raw code yourself.

What's the fastest way to find which change broke AI-generated code?

If it's a regression, use git bisect. It performs a binary search over your commit history by checking out a commit halfway between a known-good and known-bad point and asking you to mark it good or bad, narrowing a thousand commits to one culprit in about ten tests. This works especially well on AI-agent codebases since agents tend to commit in small, frequent increments.

Should you let the AI just fix the bug for you?

Not immediately. Asking an agent to 'fix this' before you've localized the problem often produces a patch that changes several things at once, so if the bug disappears you won't know why, and if it doesn't you've muddied the diff. Use the AI to explain and instrument first, form one hypothesis, test it, then ask for a fix targeted at that specific cause.

Where should you add logging when debugging AI-generated code?

At the boundaries of the suspect function only: the values coming in, the values going out, and any point where shared state is read or written. Blanket logging across a whole file buries the signal. Ask the AI to add temporary, clearly labeled logs at those boundaries without changing any logic, then remove them once the hypothesis is confirmed.

When should you regenerate AI code instead of debugging it further?

Only after you've localized the bug to a specific function and understand why it fails. At that point, handing the AI a tight spec, the failing input, the expected output, and the reason the current version breaks, and letting it regenerate can be faster than patching. Regenerating before localizing just produces a new version with the same blind spots.

The same care applies when the work is refactoring rather than debugging. See how to use AI to refactor legacy code.

How did this land?

About the author

Steve Jefferson
Steve Jefferson

Developer Advocate

Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.

Share

Get the next post in your inbox

One email a month. Product updates, engineering posts, and the best of Built with Swarmz.

I agree to receive emails about AI building tips and Swarmz product news. Unsubscribe any time.