The Problem Nobody Talks About
You paste an error into an AI chat. It gives you a fix. You apply the fix. The error changes shape. You paste the new error. Repeat five more times and now the codebase is subtly worse than when you started, you still do not know why it broke, and your git diff is a mess of half-understood patches.
This is not a hypothetical. It is the default experience when engineers treat AI as a search engine for answers rather than a tool for reasoning. The failure mode is not that AI is wrong, though it often is. The failure mode is that the workflow itself is broken: you are outsourcing diagnosis to a system that has no idea what your code actually does at runtime.
A proper AI-assisted debugging workflow does not look like "paste error, accept fix." It looks more like scientific debugging, except AI accelerates the hypothesis-generation and explanation steps while you stay in control of the diagnosis. That split matters. This post walks through exactly how to do it, with concrete scenarios and code.
Why AI Is Genuinely Useful for Debugging
Before the workflow, it is worth being clear about where AI actually helps and where it does not.
AI is good at:
- Explaining what an error message means in context you provide
- Generating hypotheses for what could cause a symptom
- Writing small diagnostic snippets (log statements, minimal repros)
- Summarising stack traces across multiple languages or unfamiliar frameworks
- Spotting obvious logic errors when you paste a function and describe expected vs actual behaviour
AI is bad at:
- Knowing the runtime state of your system
- Understanding your data model, your database schema, or your deployment topology without you telling it
- Distinguishing between five similar hypotheses without you running experiments
- Remembering what it said ten messages ago in a long chat
The useful mental model: AI is a very well-read pair programmer who has never seen your codebase and cannot touch your terminal. You have to be their hands, their eyes, and their memory. When that split is clear, the workflow snaps into place.
Phase 1: Write the Bug Report Before Asking Anything
The single most impactful habit change is this: write a structured bug report for yourself before opening an AI chat. Not a Jira ticket, not an essay. A short structured document that forces you to think.
A minimal bug report has four fields:
Symptom: What you observed, exactly. Include error text verbatim.
Expected: What should have happened.
Conditions: Inputs, environment, state, anything you know differs from normal.
Ruled out: What you already tried and what happened when you tried it.Here is a realistic example:
Symptom: POST /api/cv/parse returns 500 with body { "error": "Cannot read properties
of undefined (reading 'sections')" } on files > 200 KB. Works fine below that.
Expected: Parsed CV object returned for all valid PDFs regardless of file size.
Conditions: Node 20.14, pdf-parse 1.1.1. Happens locally and on staging. File is valid
PDF (opens fine in Acrobat). Does not happen with test files in /fixtures.
Ruled out: - Multer file size limit: set to 10 MB, logs confirm file arrives in full.
- PDF corruption: ran pdfinfo on the failing file, no errors.
- The 200 KB number: tested with 195 KB (fine), 205 KB (breaks).This discipline does two things. First, it often surfaces the answer before you even ask AI, because writing "ruled out" forces you to actually rule things out. Second, when you do paste this into an AI chat, you get dramatically better hypotheses because the model is not working from a one-line error message.
Phase 2: Use AI to Generate Hypotheses, Not Answers
With the bug report written, the prompt you send AI is not "how do I fix this?" It is "given the symptom and what I have ruled out, what are the most likely root causes? List them ordered by probability."
That framing does something important. It treats the AI response as a differential diagnosis list, not a fix. You are going to test each hypothesis yourself. The AI is not fixing the bug; it is helping you decide which rock to look under first.
For the file-size example above, a well-prompted response might return:
- The PDF parser uses a synchronous buffer that behaves differently above a certain size threshold (pdf-parse has a known issue with PDFs that have XRef streams above ~128 KB).
- A regex or string operation somewhere in the parsing pipeline has O(n) or O(n²) behaviour that causes it to hit a V8 string limit on larger inputs.
- The
sectionskey is populated by a downstream step that is skipped when processing time exceeds a timeout you have not noticed. - The file content includes characters or encoding that appears only in larger, real-world PDFs but not in your test fixtures.
Hypothesis 1 is specific and verifiable. Hypothesis 3 is worth checking because it would be invisible without looking. You now have a ranked list of experiments to run, not a patch to blindly apply.
Phase 3: Write Diagnostic Code, Not Fix Code
This is where most people go wrong. When a hypothesis looks plausible, the temptation is to ask AI to write a fix. Instead, ask it to write a diagnostic.
The distinction is subtle but critical. A diagnostic confirms or denies the hypothesis. A fix assumes the hypothesis is correct and changes production code. If the hypothesis is wrong, the fix makes things worse and you have lost the original clean state.
For hypothesis 1 from above, the diagnostic is not "patch pdf-parse." It is:
// diagnostic.ts, run this against the failing file, do not ship
import fs from "fs";
import pdfParse from "pdf-parse";
const filePath = process.argv[2];
const buffer = fs.readFileSync(filePath);
console.log("Buffer size (bytes):", buffer.byteLength);
console.log("Has XRef stream:", buffer.includes(Buffer.from("xref")));
const result = await pdfParse(buffer, {
// Disable rendering to isolate parsing from rendering step
pagerender: undefined,
});
console.log("Top-level keys on result:", Object.keys(result));
console.log("result.text length:", result.text?.length ?? "undefined");Run this on both the passing and failing file. Now you have facts, not assumptions. The AI helped you write the diagnostic quickly, but the diagnosis is yours.
If the diagnostic shows that result.text is present and correct but sections is still undefined, hypothesis 1 is eliminated. You move to the next one. This is the loop:
hypothesis → diagnostic → result → (confirmed: fix) or (denied: next hypothesis)AI accelerates the hypothesis and diagnostic-writing steps. You run the experiments.
Phase 4: Constrain the Context Window, Not Your Understanding
One reason AI-assisted debugging goes wrong is that people let the conversation grow too long. By message 15, the model has lost the thread of what was established earlier, is contradicting itself, and is producing fixes that undo things you already confirmed work.
The practical rule: a single debugging thread should cover one hypothesis at a time. When you have confirmed or denied a hypothesis, start a fresh message (or a fresh chat) summarising the current known state before moving to the next one.
A context summary looks like this and takes 30 seconds to write:
Known state:
- File arrives complete (confirmed via Multer debug log)
- pdf-parse returns correct text for large files (confirmed via diagnostic)
- The crash happens at src/lib/cv/parser.ts:line 84 where sections is accessed
- sections is populated by extractSections(), called at line 71
- extractSections() has not yet been tested in isolation
Next hypothesis: extractSections() returns undefined (not an empty array) for PDFs
whose text content exceeds a threshold it was never tested against.Paste that summary into the new message and ask for a targeted diagnostic for the next hypothesis. The model now has precise, verified context and will produce a tighter response.
This matters especially when you are using a tool like Claude in the terminal (Claude Code) or Copilot Chat inside VS Code, where the context window is shared with the file tree and diff. Long, rambling sessions cause the model to start guessing about things you already know. Short, fresh contexts with explicit state keep it sharp.
Phase 5: Read the Fix Before You Run It
When you have confirmed a hypothesis and AI proposes a fix, read it in full before applying anything. This sounds obvious. It is constantly skipped.
A few things to check explicitly:
Does it handle the error case or hide it? A fix like this hides the bug:
// Bad: this masks the problem rather than solving it
const sections = extractSections(text) ?? [];If extractSections returns undefined due to a parsing failure, replacing undefined with an empty array makes the 500 go away but returns an empty CV object to the user. The bug is now silent and much harder to catch.
A real fix addresses why extractSections returns undefined:
// Better: understand the return type contract first
function extractSections(text: string): Section[] {
if (!text || text.trim().length === 0) {
// This is the actual root cause: large PDFs with complex layouts
// produce empty text from pdf-parse when XRef streams are not decoded
throw new Error(`extractSections: empty text input (file size may require pdf.js)`);
}
// ...
}Does it introduce a regression? Ask AI explicitly: "Does this change affect any of the cases that currently pass?" A two-sentence answer is fine. You are just prompting it to look for side effects it may not have flagged.
Is there a test I should write before applying this? For a logic fix, the answer is almost always yes. Ask AI to write the test first. If the test fails before the fix and passes after, you have confirmed the fix is correct and you have a regression guard. If the test passes before the fix, the fix is not testing what you think it is.
// test written before the fix, using Vitest
import { describe, it, expect } from "vitest";
import { extractSections } from "../src/lib/cv/parser";
describe("extractSections", () => {
it("throws a descriptive error rather than returning undefined on empty text", () => {
expect(() => extractSections("")).toThrowError(/empty text input/);
});
it("returns an empty array for valid text with no recognisable section headers", () => {
expect(extractSections("John Smith\nSome random text")).toEqual([]);
});
});Run the tests. Red. Apply the fix. Run again. Green. Now you understand what you changed and why.
Phase 6: The Gotcha Nobody Warned Me About
Here is something I learned the hard way: AI is confidently wrong about version-specific behaviour.
A debugging session a while back involved a subtle difference in how ReadableStream is handled in Node 18 versus Node 20. I had a streaming response from an external API that was working fine in development (Node 20) and silently dropping bytes in a staging environment (Node 18.19). I described the symptom to Claude and got a confident explanation involving the highWaterMark option of Transform streams.
I spent two hours chasing that. It was plausible. The explanation was technically correct for a different class of stream bug. But it was not the root cause.
The actual cause was a change in how Node 18 handles backpressure on pipeline() with async generators, specifically a behaviour that was patched in Node 20.3. I only found this by looking at the Node.js changelog directly.
The lesson: when AI gives you a hypothesis that involves low-level runtime, platform, or library internals, verify against the official changelog or source code, not just the AI's explanation. Use AI to narrow the search space ("what changed in Node streams between 18 and 20?") and then go read the actual docs or release notes yourself.
AI is excellent at knowing that something changed. It is less reliable about knowing exactly what changed and when.
Applying This to Different Bug Types
The core loop (report, hypothesise, diagnose, fix, test) applies to any bug, but the prompts you use differ by category.
Async and concurrency bugs: Describe the exact sequence of events including timing. Ask AI for race condition hypotheses explicitly. These bugs almost never appear in isolation and AI needs the sequence, not just the error.
node --require ./debug-timestamps.js src/worker.js// debug-timestamps.ts, monkey-patch console to add microsecond timestamps
const orig = console.log.bind(console);
console.log = (...args) => orig(`[${Date.now()}]`, ...args);Type errors in TypeScript: Paste the full error including the path of type instantiations. The one-liner TypeScript error is often not the actual problem; the five lines of "Type X is not assignable to type Y" trace tell the real story. AI is very good at reading those traces if you give it the full output.
Database query performance: Do not paste the query and ask "is this fast?" Ask: "Given this EXPLAIN ANALYSE output, what are the most likely causes of the sequential scan?" Then paste the actual plan.
EXPLAIN (ANALYSE, BUFFERS, FORMAT TEXT)
SELECT cv.id, cv.user_id, s.name
FROM cvs cv
JOIN cv_sections s ON s.cv_id = cv.id
WHERE cv.user_id = $1
ORDER BY cv.created_at DESC;Give AI the plan output, not just the query. The query tells it nothing about what the optimiser actually did.
CSS and layout bugs: Screenshot plus a minimal HTML/CSS repro is far more useful than a paragraph description. Describe what you see versus what you expect in terms of box model, not in terms of "it looks wrong."
A Practical Checklist
When you sit down to debug with AI, run through this before you open the chat:
- Write the four-field bug report (symptom, expected, conditions, ruled out).
- Confirm you have already ruled out the obvious candidates (logs, network tab, database query counts).
- Paste the bug report and ask for a ranked hypothesis list, not a fix.
- For each hypothesis, ask for a diagnostic snippet, not a fix.
- Run the diagnostic yourself. Record the result.
- When a hypothesis is confirmed, ask for a fix and read it in full before applying.
- Ask AI to write a failing test before you apply the fix.
- Apply the fix, run the test, confirm green.
- Start a new message for any subsequent hypothesis with a clean context summary.
- For anything involving specific library versions or runtime internals, verify against official docs.
That is it. The workflow is not complex. The discipline is in following it when you are tired, the deadline is close, and the temptation is to just paste the error and hit enter.
Key Takeaways
- AI is a hypothesis generator and code writer, not a debugger. You are the debugger.
- A structured bug report written before you open AI chat improves every subsequent step.
- Ask for diagnostics before fixes. Diagnostics confirm; fixes assume.
- Keep context windows short and anchored. Summarise known state at the start of each new thread.
- Read every fix before applying it. Check whether it solves the problem or hides it.
- Write the failing test before the fix, not after.
- Version-specific and runtime-internal explanations from AI require independent verification.
- The workflow adds five to ten minutes upfront and saves hours of backtracking.
The engineers who benefit most from AI debugging tools are not the ones who trust them most. They are the ones who know exactly where to stop trusting them and pick up the investigation themselves.