Debugging is where Claude Code truly shines. Instead of staring at cryptic error messages or stepping through debuggers line by line, you can leverage Claude's ability to read stack traces, analyze code paths, and suggest fixes—all while explaining the underlying issue.
This lesson covers practical debugging workflows: how to share errors effectively, apply the TDD debugging loop, perform root cause analysis, and recognize when to break out of unproductive fix-loops.
Learning Objectives
- Share errors with Claude effectively using stack traces and screenshots
- Apply the TDD debugging loop: error → fix → verify → repeat
- Perform root cause analysis instead of suppressing symptoms
- Recognize and break out of fix-loops using context resets
- Use Bash tools for debugging (logs, git blame, git bisect)
- Debug visual issues using screenshots and browser tools
Sharing Errors with Claude
The first step in any debugging session is communicating the problem clearly. Claude Code can't see your screen unless you share the error output.
What to Share
When you encounter an error, provide:
- The full error message (copy-paste the entire output)
- Stack trace (if available)
- What you were trying to do (context helps Claude understand intent)
- Whether the error is reproducible (always, sometimes, or once)
Good example:
I'm seeing an error when I run npm test:
FAIL src/auth/login.test.ts
● Login component › should validate email format
expect(received).toHaveBeenCalledWith(expected)
Expected: {"error": "Invalid email format"}
Received: {"error": undefined}
12 | fireEvent.click(submitButton);
13 |
> 14 | expect(mockOnError).toHaveBeenCalledWith({
| ^
15 | error: "Invalid email format"
16 | });
at Object.<anonymous> (src/auth/login.test.ts:14:25)
This started happening after I added the Zod validation schema.
Poor example:
My tests are broken. Can you fix them?
The good example includes the test file, the exact assertion that failed, the stack trace line numbers, and context about recent changes. This gives Claude everything needed to diagnose the issue.
Using Screenshots for Visual Bugs
For UI issues, a screenshot is worth a thousand words:
The login button is overlapping the input field on mobile. See screenshot:
[paste image]
This only happens on screens under 640px width.
Claude can analyze UI screenshots and suggest CSS fixes based on what it sees.
Pro tip: Use descriptive filenames for screenshots like login-button-overlap-mobile.png instead of Screenshot_2026-02-08.png. If you paste multiple images, Claude can distinguish them more easily.
The TDD Debugging Loop
Test-Driven Development (TDD) isn't just for writing new features—it's a powerful debugging workflow. The pattern is simple:
Share the failing test output with Claude.
Ask for fix suggestions (Claude will read the test, the error, and the relevant code).
Apply the fix (Claude can edit the code directly or suggest changes).
Verify with tests (run the test suite again).
Repeat if needed (if the test still fails, share the new error and iterate).
Example: Fixing a Failing Unit Test
Prompt:
I'm getting a test failure:
FAIL src/utils/formatCurrency.test.ts
● formatCurrency › should handle negative values
expect(received).toBe(expected)
Expected: "-$10.00"
Received: "$-10.00"
18 | it('should handle negative values', () => {
19 | const result = formatCurrency(-10);
> 20 | expect(result).toBe('-$10.00');
| ^
21 | });
Can you fix the formatCurrency function to handle negatives correctly?
Claude's response:
Claude reads the test file, finds src/utils/formatCurrency.ts, identifies the issue (the minus sign is appearing after the dollar sign), and applies a fix:
// src/utils/formatCurrency.ts
export function formatCurrency(amount: number): string {
const isNegative = amount < 0;
const absoluteAmount = Math.abs(amount);
const formatted = `$${absoluteAmount.toFixed(2)}`;
return isNegative ? `-${formatted}` : formatted;
}Verification:
Run the test again to verify:
npm test formatCurrency.test.ts
If the test passes, you're done. If it still fails, share the new error and continue the loop.
Why This Works
The TDD loop keeps Claude grounded in reality. Instead of guessing whether a fix worked, you have automated verification. This prevents "false positives" where Claude thinks a fix is correct but the tests prove otherwise.
Key Takeaway
The TDD debugging loop (error → fix → verify) ensures every change is validated by tests, preventing guesswork and regressions.
Root Cause Analysis
The difference between a junior developer and a senior developer is often how they approach debugging. Juniors suppress symptoms. Seniors find root causes.
Claude Code can help with root cause analysis—but you need to ask the right questions.
Address the Root Cause, Don't Suppress the Error
Bad prompt:
I'm getting a "Cannot read property 'id' of undefined" error. Can you add a null check?
Better prompt:
I'm getting a "Cannot read property 'id' of undefined" error when rendering the UserProfile component. Can you trace where the user object is supposed to be populated and why it might be undefined?
The first prompt asks Claude to slap a band-aid on the symptom. The second prompt asks for investigation.
Claude's investigation might reveal:
- The API call that fetches the user is failing silently
- The component renders before the data loads (race condition)
- The Redux selector is selecting from the wrong slice of state
These are root causes. Adding a null check would hide the real problem.
Sharing Steps to Reproduce
If the error is intermittent or context-dependent, share the exact steps:
I'm seeing a 500 error from the /api/checkout endpoint, but only when:
1. The cart has more than 5 items
2. At least one item has a discount code applied
3. The user is logged in with a free-tier account
Steps to reproduce:
1. Log in as testuser@example.com
2. Add 6 items to cart
3. Apply discount code "SAVE10" to one item
4. Click "Checkout"
5. Error appears in browser console
Stack trace:
[paste full stack trace]
This level of detail helps Claude narrow down the problem space. It's the difference between "something's wrong with checkout" and "the discount calculation logic fails for free-tier users with 5+ items."
Intermittent Errors
For intermittent bugs, mention:
- Frequency: "Happens about 1 in 10 times" vs. "Happens every time on Safari"
- Environment: "Only in production" vs. "Only on my machine"
- Timing: "Only after the app has been idle for 10+ minutes"
Example:
The dashboard component crashes intermittently with "Maximum update depth exceeded". This happens maybe 20% of the time when navigating from the home page. I suspect it's a state update loop in useEffect, but I'm not sure which one.
Can you review the useEffect hooks in src/components/Dashboard.tsx and identify any that might cause infinite loops?
Claude will scan for missing dependencies, state updates that trigger re-renders, or effects that don't have proper cleanup.
Beware of "works on my machine": If an error only happens in production or on certain devices, share environment details (OS, browser, Node version, environment variables). Claude can spot environment-specific issues like missing polyfills or API endpoint differences.
Common Debugging Prompts
Here are high-leverage prompts for common debugging scenarios:
Tracing Code Paths
Trace the login process from the frontend LoginForm component through the API route to the database query. Where could the authentication token be getting lost?
Claude will read each file in the chain and identify where data is transformed, validated, or dropped.
Finding Recent Changes
The email notification feature was working yesterday but now it's not sending emails. Can you check what changed in the last 3 commits that might have broken it?
Claude can run git log and git diff to find recent changes to email-related code.
Understanding Unexpected Behavior
Why does the searchProducts function call filterByCategory() instead of filterByTag()? I expected it to filter by tags based on the user's selection.
Claude will read the function, explain the current logic, and suggest why the implementation doesn't match your expectation (maybe the requirements changed, or the function name is misleading).
Performance Issues
The /api/users endpoint is taking 3-4 seconds to respond. Can you identify any N+1 query problems or missing database indexes?
Claude can spot patterns like:
// N+1 query problem
const users = await db.user.findMany();
for (const user of users) {
user.posts = await db.post.findMany({ where: { userId: user.id } });
}And suggest a fix:
// Fixed with eager loading
const users = await db.user.findMany({
include: { posts: true }
});Type Errors
TypeScript is complaining that "Property 'email' does not exist on type 'User | Admin'". Can you explain why and how to fix it with a type guard?
Claude can explain union types and suggest a fix:
function sendEmail(recipient: User | Admin) {
if ('email' in recipient) {
// Type narrowed to User
sendTo(recipient.email);
}
}Breaking Out of Fix-Loops
Sometimes debugging goes in circles. Claude suggests a fix, it doesn't work, Claude suggests another fix, that breaks something else, and you're stuck in a loop.
Signs you're in a fix-loop:
- You've applied 3+ fixes to the same issue and it's still broken
- Each fix introduces a new error
- Claude starts suggesting changes that contradict earlier suggestions
- You're no longer sure what the original problem was
Strategy 1: Clear Context and Start Fresh
After 2 failed correction attempts, use /clear to reset the conversation:
/clear
I'm debugging a login issue. Here's the current state:
- Users can't log in (form submits but nothing happens)
- No errors in console or network tab
- Login worked before I refactored the auth API route
- Current code: [paste relevant files]
Can you review the auth flow and identify where the login response is getting dropped?
Starting fresh prevents Claude from being biased by failed attempts. You provide clean context and the current state of the code.
Strategy 2: Use Subagents for Investigation
If the bug requires deep investigation, use a subagent to explore while keeping your main context clean:
Main conversation:
/ask-teammate Create a "debugger" agent to investigate why the login form isn't submitting. Have them trace the form submission through handleSubmit, the API call, and the response handling.
The debugger agent can explore, read logs, and test hypotheses without polluting your main conversation. Once they find the issue, they report back and you apply the fix.
Strategy 3: Verify Assumptions
Fix-loops often happen because of false assumptions. Stop and verify:
Before we continue, let's verify some assumptions:
1. Is the API route /api/auth/login actually being called? (Check network tab)
2. Is the response status 200 or an error code?
3. Is the response body what we expect, or is it malformed?
I'll run the login flow with the browser console open and paste the results.
Verifying assumptions grounds the debugging session in facts instead of guesses.
| Stuck in Fix-Loop | Productive Debugging |
|---|---|
| Apply fix → breaks something else → apply another fix | Error → root cause analysis → single fix → verify |
| Conversation context is 50+ messages deep | Clear context after 2 failed attempts |
| Claude contradicts earlier suggestions | Each fix is validated by tests or manual verification |
| You're not sure what the current problem is | Problem is clearly defined at every step |
Visual Debugging with Images
UI bugs are hard to describe with words. Screenshots make debugging visual issues 10x faster.
Example: Layout Bug
Prompt:
The pricing cards are misaligned on mobile. See screenshot:
[paste screenshot showing cards at different heights]
They should all be the same height regardless of content length.
Claude can analyze the screenshot and suggest CSS fixes:
/* Add to pricing card container */
.pricing-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
align-items: stretch; /* Makes all cards equal height */
}
.pricing-card {
display: flex;
flex-direction: column;
}
.pricing-card-footer {
margin-top: auto; /* Pushes button to bottom */
}Using Browser DevTools with Claude
For live debugging, use the Claude in Chrome extension (if available) or share DevTools screenshots:
The hover state on the CTA button isn't working. See DevTools screenshot showing the computed styles:
[paste screenshot of DevTools showing button:hover styles being overridden]
It looks like a specificity issue. Can you fix it?
Claude can spot that .btn-primary:hover is being overridden by .button.btn-primary and suggest increasing specificity or using !important (as a last resort).
Using Bash for Debugging
Claude has access to Bash commands, which are invaluable for debugging:
Check Logs
Check the application logs for errors in the last hour.
Claude runs:
tail -n 1000 /var/log/app.log | grep -i errorAnd surfaces relevant error messages.
Run with Verbose Flags
Run the build script with verbose logging to see where it's failing.
Claude runs:
npm run build --verboseAnd analyzes the output to identify the failing step.
Git Blame
Who last modified the authentication middleware? I think a recent change broke session handling.
Claude runs:
git blame src/middleware/auth.tsAnd identifies the commit and author.
Git Bisect
For bugs introduced by recent commits:
The dashboard was working 2 weeks ago but is broken now. Can you use git bisect to find the commit that introduced the bug?
Claude can automate git bisect:
git bisect start
git bisect bad HEAD
git bisect good a1b2c3d # Last known good commit
# Claude runs tests at each step to identify the bad commitThis is especially useful for large codebases where manually testing each commit would take hours.
When to use Bash for debugging: Log analysis, environment checks, git history, running tests with specific flags, checking file permissions, verifying API responses with curl.
Exercise: Debug a Real Error Using the TDD Loop
Let's practice the TDD debugging loop with a realistic scenario.
Fix the Failing Integration Test
beginnerScenario:
You're working on an e-commerce app. The integration test checkout.test.ts is failing with this output:
FAIL tests/integration/checkout.test.ts
● Checkout flow › should create order and charge payment
expect(received).toBe(expected)
Expected: "success"
Received: "failed"
45 | const response = await request(app)
46 | .post('/api/checkout')
47 | .send({ cartId: cart.id, paymentMethod: 'card' });
> 48 | expect(response.body.status).toBe('success');
| ^
49 | });
at Object.<anonymous> (tests/integration/checkout.test.ts:48:34)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Your task:
- Share the error with Claude and ask for initial diagnosis
- Ask Claude to trace the checkout flow (
POST /api/checkout→ payment processing → database) - Apply the suggested fix
- Re-run the test
- If it still fails, share the new error and iterate
Prompt to get started:
I have a failing integration test for the checkout flow. Here's the test output:
[paste the output above]
The test is calling POST /api/checkout with a cartId and paymentMethod, but the response status is "failed" instead of "success".
Can you trace the checkout flow and identify why it's failing?
What Claude might discover:
- The payment API requires a
customerIdfield that the test isn't providing - The cart total calculation is incorrect (tax not included)
- The payment provider is in test mode but the API key is for production
- A validation error is being caught and returned as
{ status: "failed" }without details
Once Claude identifies the root cause, apply the fix, re-run the test, and verify it passes.
Summary
Debugging with Claude Code transforms error messages from frustrating dead-ends into structured problem-solving sessions. By sharing complete error context, applying the TDD loop, focusing on root causes instead of symptoms, and knowing when to reset context, you can resolve bugs faster and with more confidence.
Key practices:
- Share full error messages, stack traces, and reproduction steps
- Use the TDD loop (error → fix → verify) to validate every change
- Ask Claude to trace code paths and identify root causes
- Break fix-loops by clearing context or using subagents for investigation
- Use screenshots for visual bugs and Bash for log analysis
- Verify assumptions when stuck instead of guessing
Key Takeaway
Effective debugging with Claude Code is about clear communication (share full errors), systematic investigation (TDD loop, root cause analysis), and knowing when to reset (break fix-loops before they spiral).
Next Steps
In the next lesson, we'll explore Refactoring with Confidence — how to rename symbols across files, extract reusable components, migrate deprecated APIs, and execute large-scale refactoring safely using checkpoints and incremental verification.