DEV Community

Cover image for 10 Claude Prompts for Faster Debugging (With Examples)
Dev Prompts
Dev Prompts

Posted on

10 Claude Prompts for Faster Debugging (With Examples)

Debugging is not about finding the bug. It is about finding the bug fast.

The actual fix is usually one line. Maybe two. The problem is everything before it — the hour you spend reading the wrong file, the three hypotheses that turned out wrong, the log statements you added and then forgot to remove. Stack traces point at symptoms, not causes. Error messages describe what broke, not why. And the real culprit is almost always three function calls away from where the exception surfaced.

The loop that kills time: read error → guess cause → add log → re-run → wrong guess → repeat. You are not debugging. You are narrowing down, very slowly, by trial and error. The bottleneck is not the fix. It is the diagnosis.

These 10 Claude prompts for debugging shortcut the narrowing-down phase. Each one targets a specific debugging scenario — cryptic stack traces, flaky tests, performance regressions, concurrency bugs — and gives Claude enough structure to produce a diagnosis, not just a restatement of the error. Copy-paste ready, with realistic examples showing what useful output actually looks like. If you use the code review prompts from the previous article to catch bugs before they ship, these are for the ones that got through anyway.


Before You Start: How to Feed Claude Error Context

No setup required. Claude works directly in the chat window, but the quality of the diagnosis depends entirely on the quality of the context you paste in.

Stack trace: Copy the full trace, not just the last line. Claude needs the call chain to distinguish root cause from symptom. The frame where the exception was thrown is rarely the frame where the bug lives.

Logs: Include 10–20 lines around the error timestamp. Trim irrelevant noise but keep the lines immediately before the failure — they often contain the state that caused it.

Relevant code: Paste the function or file the stack trace points to. If you are not sure which, paste the function you think is involved and the one that calls it. Two functions is almost always enough.

Repro steps + environment: A one-line description of how to trigger the bug beats nothing. "Click checkout with an expired card — expected error modal, got blank screen." Add language version and framework version in the same line. Claude can hallucinate version-specific behavior if you leave it unanchored.

The golden rule: more context equals a better diagnosis. A stack trace alone gets you a guess. A stack trace plus code plus repro steps gets you a root cause.


The 10 Prompts

1. Interpret This Stack Trace and Find the Root Cause

The Prompt

Analyze this stack trace and identify the root cause — not just the line that threw, but the code that caused it. Distinguish between the symptom (where the error surfaced) and the cause (where the real bug is). Ignore framework internals. Focus on my code. Show me the specific line responsible, explain why it causes this error, and suggest a fix.

Stack trace:
[paste here]

Relevant code:
[paste the file or function the trace points to]
Enter fullscreen mode Exit fullscreen mode

What it does
Takes a raw stack trace and maps it to the actual root cause instead of the line that threw. Most stack traces point at library internals — Claude cuts through them to find the caller-side mistake.

Example output

Root cause: line 12 in process_order(), not line 47 where the TypeError surfaced.

  # orders/processor.py, line 12
  total = sum(item["price"] for item in cart_items)

  cart_items is None here  passed from get_cart() on line 8, which
  returns None when the cart session has expired instead of an empty list.

  The TypeError on line 47 ("NoneType is not iterable") is the symptom.
  The bug is the missing None check in get_cart()'s return path.

Fix:
  def get_cart(session_id: str) -> list:
      cart = db.get(f"cart:{session_id}")
      return cart if cart is not None else []

  # Or at the call site:
  cart_items = get_cart(session_id) or []
Enter fullscreen mode Exit fullscreen mode

Why it works
Explicitly instructing Claude to separate symptom from cause prevents it from just restating the error message. Without that constraint, Claude defaults to explaining the exception type — not where it came from.

Pro tip — Add "ignore framework internals, focus only on my application code" when debugging Django, Rails, or Spring traces where the framework stack is 40 frames deep.


2. Explain Why This Error Happens, Not Just What It Says

The Prompt

I'm seeing this error. Explain why it happens at the mechanism level — not just what the message means, but what state in my code or environment causes it. Then tell me the 3 most likely causes given the code I've pasted, ranked by probability.

Error:
[paste error message]

Code:
[paste relevant function or file]

Language/framework: [e.g. Node.js 20, Express 4.18]
Enter fullscreen mode Exit fullscreen mode

What it does
Translates cryptic error messages into plain-English explanations of the underlying mechanism. Goes beyond "what does this mean" to "what state in my code triggered this."

Example output

ECONNRESET means the remote server closed the TCP connection while your
code was still reading from it. This is different from a timeout — the
connection was open, then forcibly terminated mid-transfer.

3 most likely causes in your code, ranked:

1. (Most likely) Missing keepalive config on your http.Agent.
   Your createConnection() on line 14 uses the default agent, which
   doesn't send TCP keepalives. The upstream server is closing idle
   connections after ~30s. Fix: pass keepAlive: true to the agent.

2. The upstream server is crashing or restarting mid-request. The
   response body on line 29 is being consumed in a stream — if the
   server dies, you get ECONNRESET instead of a proper error code.
   Fix: add an 'error' event handler on the response stream.

3. Rate limiting at the network level (less likely given your logs).
   Some load balancers reset connections when a threshold is hit
   without returning an HTTP 429. Check upstream logs if #1 doesn't fix it.
Enter fullscreen mode Exit fullscreen mode

Why it works
"Not just what it says" is the critical constraint. Without it, Claude produces dictionary definitions. The ranked-probability format makes the output actionable — you start with #1, not a list of ten equally-weighted possibilities.

Pro tip — Include the language and framework version. Error messages and their underlying causes change across versions. ECONNRESET in Node 14 has a different default behavior than Node 20.


3. Find the Bug: Here's What I Expected vs. What Actually Happened

The Prompt

Here is a function, what I expected it to do, and what it actually does. Find the specific line(s) causing the discrepancy. No stack trace — this is a logic bug that doesn't throw. Show the exact bug, explain why it produces the wrong output, and fix it.

Code:
[paste function]

Input: [paste sample input]
Expected output: [what you thought would happen]
Actual output: [what actually happened]
Enter fullscreen mode Exit fullscreen mode

What it does
Diagnoses logic bugs that don't throw exceptions — the ones where the code runs fine and produces the wrong answer. The expected/actual framing gives Claude a concrete test oracle to reason against.

Example output

Bug: line 8, the comparison function does not handle ties.

  // Current
  const sorted = users.sort((a, b) => b.score - a.score)

  // When a.score === b.score, this returns 0.
  // JavaScript's Array.sort() is not guaranteed stable in all engines
  // for elements that compare equal — the relative order of tied users
  // is undefined, which is why the order changes on each run.

Fix (stable sort on ties  break by name alphabetically):
  const sorted = users.sort((a, b) => {
    if (b.score !== a.score) return b.score - a.score
    return a.name.localeCompare(b.name)
  })

  // If you need original-order stability for ties, use:
  const indexed = users.map((u, i) => ({ ...u, _i: i }))
  const sorted = indexed.sort((a, b) =>
    b.score !== a.score ? b.score - a.score : a._i - b._i
  )
Enter fullscreen mode Exit fullscreen mode

Why it works
The expected/actual framing constrains Claude to a specific outcome to reason toward. "Find bugs" is open-ended and produces generic feedback. "Here is what I expected, here is what I got" is a test assertion — Claude solves backward from the failing case.

Pro tip — Include two or three sample input/output pairs, not just one. Edge cases in the data often reveal the bug faster than the primary example.


4. Debug This Flaky Test

The Prompt

This test sometimes passes, sometimes fails. I cannot reproduce it reliably. Analyze the test and the code it's testing for sources of nondeterminism. Check for: shared mutable state between tests, timing dependencies (sleeps, timeouts, async ordering), test-order dependencies, uncontrolled randomness, environment variable leakage, and database/filesystem state not cleaned up between runs.

Test code:
[paste test]

Code under test:
[paste relevant functions]

Failure output (when it fails):
[paste the error]

Passing output (when it passes):
[paste if different]
Enter fullscreen mode Exit fullscreen mode

What it does
Identifies the nondeterminism source in tests that fail inconsistently. Flaky tests are usually one of six things — this prompt makes Claude check all six instead of guessing.

Example output

Source of flakiness: database state not cleaned up between test runs.

  # test_user_signup.py, line 31
  def test_signup_duplicate_email():
      create_user(email="test@example.com")
      with pytest.raises(DuplicateEmailError):
          create_user(email="test@example.com")

  This test passes when run in isolation. It fails when test_login_success
  runs first  that test also calls create_user("test@example.com") and
  does not clean up. On the second call here, the first insert from
  test_login_success is already in the DB, so the "duplicate" check fires
  on the first create_user() instead of the second.

  The test also has an implicit ordering dependency on test_login_success.
  In pytest, test execution order is deterministic by default but changes
  with -p randomly or across machines.

Fix:
  @pytest.fixture(autouse=True)
  def clean_users_table(db_session):
      yield
      db_session.execute("DELETE FROM users WHERE email LIKE '%@example.com'")
      db_session.commit()

  # Or use a transaction rollback fixture (preferred):
  @pytest.fixture(autouse=True)
  def rollback(db_session):
      yield
      db_session.rollback()
Enter fullscreen mode Exit fullscreen mode

Why it works
Enumerated category lists act as a diagnostic checklist. Without the list, Claude picks the first plausible explanation. With it, Claude systematically checks each category before settling on a cause — which matters because flaky tests are often caused by the non-obvious category.

Pro tip — Include both the passing and failing output when available. The diff between them is often the fastest path to the root cause — the failure usually contains state from a previous test that the passing run doesn't have.


5. What Changed That Could Have Caused This Regression?

The Prompt

A regression appeared after the latest deploy. Here is the symptom and here is the git diff (or list of recent commits). Identify which change most likely introduced the bug. Explain the mechanism — how does that change produce this symptom? Then tell me what to verify to confirm it's the right suspect.

Symptom:
[describe what broke — error message, wrong behavior, performance change]

Git diff or recent commits:
[paste diff or git log --oneline output]

Affected code (if known):
[paste relevant function]
Enter fullscreen mode Exit fullscreen mode

What it does
Narrows from "something broke after the last deploy" to "this specific change broke it, for this specific reason." Combines the diff with the symptom to do targeted bisection instead of reviewing the whole diff generically.

Example output

Most likely culprit: the ORM change in models/order.py (commit a3f9c2).

  # Before
  orders = Order.objects.filter(user=user)

  # After
  orders = Order.objects.filter(user=user).prefetch_related("items__product")

  The prefetch_related call looks like an optimization, but it's loading
  all items and products for every order in the queryset — not just the
  orders on the current page. With 500 orders per user, this is now
  fetching 8,000–12,000 rows on every request instead of 20–40.

  This matches your symptom: response time went from ~180ms to ~3.4s
  exactly when this commit deployed, and only for users with large order histories.

To confirm:
  1. Run EXPLAIN ANALYZE on the query for a user with 200+ orders
  2. Add Django Debug Toolbar to the endpoint and count the queries
  3. Check if the issue is absent for new users (< 10 orders) — if so,
     it confirms the data-volume dependency
Enter fullscreen mode Exit fullscreen mode

Why it works
Giving Claude both the diff and the symptom lets it do targeted causal reasoning. A diff alone produces a generic review. A symptom alone produces speculation. Together, they constrain the problem space to where the diff's changes intersect with the symptom's behavior.

Pro tip — Use git log --oneline -20 as the input when you don't have a specific diff. Claude can rank which commits are worth investigating based on the files touched relative to the symptom.


6. Debug This Performance Bottleneck

The Prompt

This code is slow. Here are the symptoms (latency numbers, profiler output, or observed behavior). Identify the bottleneck. Check for: algorithmic complexity issues, unnecessary or sequential I/O that could be parallelized, N+1 queries, blocking calls in async code, memory allocation patterns causing GC pressure, and missing indexes. Show the specific line(s) causing the slowness and suggest a fix.

Code:
[paste slow function or endpoint handler]

Performance symptoms:
[e.g. "P99 latency 3.2s, was 200ms before commit abc123" or profiler output]

Language/framework: [e.g. Go 1.22, Gin]
Enter fullscreen mode Exit fullscreen mode

What it does
Identifies the specific bottleneck in slow code. Covers the categories that are responsible for 90% of real performance issues: I/O patterns, query problems, algorithmic complexity, and memory.

Example output

Bottleneck: three sequential database calls on lines 18, 24, and 31 that can run concurrently.

  // Current — sequential, ~900ms total
  user, err := db.GetUser(ctx, userID)          // ~300ms
  orders, err := db.GetOrders(ctx, userID)      // ~300ms
  prefs, err := db.GetPreferences(ctx, userID)  // ~300ms

  These three queries are independent  none depends on the result of
  another. Running them sequentially is costing you ~600ms of unnecessary
  wait time.

Fix (parallel with errgroup):
  var (
      user  *User
      orders []Order
      prefs  *Preferences
  )
  g, gctx := errgroup.WithContext(ctx)
  g.Go(func() error { user, err = db.GetUser(gctx, userID); return err })
  g.Go(func() error { orders, err = db.GetOrders(gctx, userID); return err })
  g.Go(func() error { prefs, err = db.GetPreferences(gctx, userID); return err })
  if err := g.Wait(); err != nil {
      return nil, err
  }

  Expected result: ~300ms total (limited by the slowest single query).

Secondary issue: GetOrders() on line 24 is missing a WHERE clause index.
Run EXPLAIN ANALYZE  if you see Seq Scan on orders, add:
  CREATE INDEX idx_orders_user_id ON orders(user_id);
Enter fullscreen mode Exit fullscreen mode

Why it works
Listing specific bottleneck categories prevents Claude from defaulting to generic advice ("add caching," "use a CDN"). The categories act as a scan checklist. The request for specific lines forces a concrete diagnosis rather than a performance lecture.

Pro tip — Include actual latency numbers or profiler output. "It's slow" gets generic advice. "P99 went from 200ms to 3.2s after deploying commit abc123" gets a diagnosis.


7. Debug This Async/Concurrency Bug

The Prompt

This code has a concurrency bug. Analyze it for: race conditions on shared state, deadlocks, missing awaits or unhandled promise rejections, incorrect assumptions about execution order, and goroutine/thread leaks. Trace two concurrent executions of this code step by step to show the exact interleaving that causes the bug. Then show the fix.

Code:
[paste the async or concurrent function(s)]

Symptom:
[describe what you observe — panic, wrong value, hang, intermittent failure]

Language/runtime: [e.g. Go 1.22, Node.js 20]
Enter fullscreen mode Exit fullscreen mode

What it does
Analyzes concurrent code for the class of bugs that are invisible to line-by-line reading. Race conditions, deadlocks, and ordering bugs only appear when you trace multiple execution paths simultaneously — which is exactly what this prompt forces.

Example output

Race condition: concurrent writes to the shared cache map on lines 8 and 14.

  var cache = map[string]int{}  // shared across goroutines

  func increment(key string) {
      val := cache[key]     // READ  — goroutine A
      cache[key] = val + 1  // WRITE — goroutine A
  }

Two concurrent executions:

  Goroutine A: reads cache["hits"]  5
  Goroutine B: reads cache["hits"]  5  (before A writes)
  Goroutine A: writes cache["hits"] = 6
  Goroutine B: writes cache["hits"] = 6  (overwrites A, loses one increment)

  After 2 increments: cache["hits"] == 6 instead of 7.
  At high concurrency this also triggers "concurrent map read and map write" panic.

Fix (atomic counter):
  var counters sync.Map

  func increment(key string) {
      val, _ := counters.LoadOrStore(key, new(int64))
      atomic.AddInt64(val.(*int64), 1)
  }

  // Or with a mutex if you need the full map readable:
  var (
      mu    sync.RWMutex
      cache = map[string]int{}
  )
  func increment(key string) {
      mu.Lock()
      defer mu.Unlock()
      cache[key]++
  }
Enter fullscreen mode Exit fullscreen mode

Why it works
Asking Claude to "trace two concurrent executions step by step" forces multi-path reasoning. Without that instruction, Claude reads the code sequentially and misses the interleaving. The step-by-step trace also makes the explanation immediately understandable — you see exactly where the paths collide.

Pro tip — Add "check for goroutine leaks — are there paths where a goroutine could block forever?" for Go code involving channels or context.WithCancel.


8. Debug This Third-Party API Integration Error

The Prompt

I'm getting an error from a third-party API. Here is the full HTTP request I'm sending (or the SDK call), the full response I'm receiving (headers + body), and the relevant calling code. Diagnose the integration issue. Check for: auth errors (wrong key, wrong header, wrong scope), payload format mismatches (wrong field names, wrong types, missing required fields), rate limiting, wrong API endpoint or version, and timeout configuration.

API: [name and version, e.g. Stripe API v2023-10-16]
Error response: [paste full response including headers]
Your request: [paste the outgoing request or SDK call]
Relevant code: [paste the function making the call]
Enter fullscreen mode Exit fullscreen mode

What it does
Diagnoses integration errors between your code and external APIs. API errors are notoriously misleading — a 400 Bad Request can mean a wrong field name, a missing required field, a type mismatch, or an outdated API version. This prompt makes Claude check all of them.

Example output

The 400 error is caused by a webhook signature verification mismatch.

  # Your code (line 22)
  stripe.webhook.construct_event(
      payload, sig_header, os.environ["STRIPE_WEBHOOK_SECRET"]
  )

  STRIPE_WEBHOOK_SECRET is set to your test mode webhook secret
  (starts with whsec_test_...), but the webhook is coming from a
  live mode endpoint. Stripe uses separate signing secrets for test
  and live webhooks  they are not interchangeable.

How to confirm:
  1. Go to Stripe Dashboard  Webhooks  your endpoint
  2. Check which mode the endpoint is in (test or live)
  3. Copy the correct signing secret for that mode

Also check: your endpoint is consuming the raw request body before
parsing it. If you run body-parser or any middleware that reads the
stream before this handler, the signature verification will fail
even with the correct secret.

  # Flask — must use request.get_data(as_text=True) not request.json
  payload = request.get_data(as_text=True)
Enter fullscreen mode Exit fullscreen mode

Why it works
API errors often have misleading messages that describe the consequence, not the cause. Claude cross-references the error code, the request, and the code together — something you can't do by Googling the error message alone.

Pro tip — Always include full HTTP headers in the request and response, not just the body. Auth issues hide in headers (Authorization, Stripe-Signature, X-API-Key) and are invisible if you only paste the JSON body.


9. Write a Minimal Reproduction Case for This Bug

The Prompt

Here is a bug and the code it occurs in. Write the smallest possible standalone script that reproduces the issue. Requirements: (1) it should run with zero external dependencies if possible, (2) it should reliably trigger the bug when executed, (3) it should contain only the code necessary to show the bug — strip out everything else. Show the expected vs. actual output when the script runs.

Bug description:
[describe what goes wrong]

Relevant code:
[paste the functions involved]

Language: [e.g. Python 3.12]
Enter fullscreen mode Exit fullscreen mode

What it does
Produces a minimal, standalone reproduction case — the thing you need for bug reports, teammate communication, and regression tests. Stripping out framework and dependencies isolates the core bug and often reveals it more clearly than the original code does.

Example output

# Minimal reproduction — race condition in cache invalidation
# Run with: python repro.py
# Expected: cache["value"] == 0 after both threads complete
# Actual:   cache["value"] is nondeterministic (usually 1 or 2)

import threading

cache = {"value": 0}

def increment_and_reset():
    val = cache["value"]       # read
    cache["value"] = val + 1   # write
    cache["value"] = 0         # reset

threads = [threading.Thread(target=increment_and_reset) for _ in range(100)]
for t in threads: t.start()
for t in threads: t.join()

print(f"Final value: {cache['value']}")  # should be 0, usually isn't

# Root cause: read-modify-write is not atomic.
# Two threads can read val=0, both write val=1, then both write val=0.
# But if thread A writes 1 after thread B's reset, final value is 1.
Enter fullscreen mode Exit fullscreen mode

Why it works
"Minimal" and "standalone" are the critical constraints. Without them, Claude pastes back your original code with minor edits. The minimum-dependencies constraint forces distillation to the bug's essence — which is useful for bug reports, sharing with library maintainers, and writing a regression test that will never have setup issues.

Pro tip — Add "make it runnable with zero dependencies" for maximum portability. A repro case that requires a running database is harder to share and harder to run in CI as a regression test.


10. Explain the Fix So I Can Prevent This Class of Bug in Future

The Prompt

I just fixed this bug. Explain it at the class level — not just why this specific instance happened, but what category of bug this is, why this category is common, what programming patterns or practices prevent it, and where else in my codebase the same class of bug is likely to exist. Then suggest one linting rule or test pattern that would catch it automatically.

The bug: [describe what it was]
The fix: [describe or paste what you changed]
Codebase context: [language, framework, rough size — e.g. "Python Django monolith, ~40k lines"]
Enter fullscreen mode Exit fullscreen mode

What it does
Turns a one-time fix into a permanent skill upgrade. After you find and fix a bug, this prompt extracts the generalizable lesson: what class of bug it was, why it recurs, and how to prevent the whole category rather than this one instance.

Example output

Class: Fencepost error (off-by-one in boundary conditions).

This is one of the most common bugs in any code that works with
sequences, ranges, or pagination. The name comes from the problem of
counting fence posts vs. fence sections: N sections require N+1 posts.
Developers consistently conflate "number of elements" with "last valid index."

Why it recurs:
  Arrays are 0-indexed but lengths are 1-indexed. Page numbers are
  1-indexed but offsets are 0-indexed. Date ranges use inclusive or
  exclusive bounds inconsistently. Every time you write a loop bound
  or a slice, you're making an implicit decision about which convention
  you're following  and it's easy to get it wrong silently.

Where to look in your codebase:
  Any function containing: page, offset, limit, slice, range, index,
  chunk, batch, cursor, or start/end parameters. Specifically search for:
  - `OFFSET (page - 1) * limit` — wrong if page is 0-indexed
  - `items[0:n]` — check if n is inclusive or exclusive
  - `range(1, len(items))` — check if 0 should be included

Linting rule: write a parametrized test fixture that runs every
pagination function with page=0, page=1, page=last, and page=last+1.
Fencepost bugs almost always appear at exactly these four boundaries.

  @pytest.mark.parametrize("page,expected_first_id", [
      (1, 1),     # first page
      (2, 11),    # second page
      (10, 91),   # last full page
      (11, None), # beyond last page — should return empty, not error
  ])
  def test_pagination_boundaries(page, expected_first_id):
      results = get_items(page=page, per_page=10)
      ...
Enter fullscreen mode Exit fullscreen mode

Why it works
"Class of bug" framing forces Claude to abstract from the specific instance to the reusable lesson. The "where else in my codebase" question makes the output immediately actionable — you get a search pattern you can run right now, not just a general principle.

Pro tip — Add "suggest a linting rule or static analysis check that would catch this automatically" to turn the lesson into an enforceable guardrail that catches future instances without any manual review.


The Bigger Principle

Debugging with AI is not "paste error, get fix." That works for trivial cases. For real bugs — the ones that cost hours — the value is in the narrowing. These prompts work because they give Claude a specific diagnostic task with enough context to reason about, not an open-ended "help me debug this."

The pattern is always the same: context + constraint + format. Context means the stack trace, the code, the repro steps — whatever Claude needs to reason about state and execution paths, not just syntax. Constraint means what to focus on: root cause not symptom, mechanism not definition, minimal reproduction not paraphrased description. Format means how to deliver the output: specific line numbers, step-by-step execution trace, ranked hypotheses with verification steps.

A prompt that gives Claude all three produces a diagnosis. A prompt that gives Claude one or two produces a lecture. The difference between debugging for 10 minutes and debugging for two hours is usually the quality of the question you ask first.

Build a personal prompt library for your recurring debugging scenarios. The prompts you reach for every week are worth tuning to your stack and your codebase. The ones you use once are fine as-is. Start with the one that matches your most expensive debugging pain — the class of bug that reliably costs you the most time. Build from there.

Bookmark this. Next time a stack trace lands in your terminal, paste one of these before you add a single log statement.


Want the full pack?
This article covers 10 prompts. The Claude Prompts for Developers pack includes 55 prompts across 6 categories — debugging, architecture, docs, productivity, and 5 multi-step power combos. One-time download, copy-paste ready.

Top comments (2)

Collapse
 
devprompts profile image
Dev Prompts • Edited

Context on why I wrote this: I kept getting useful-sounding but wrong answers from Claude when debugging. The fix wasn't a better model — it was a better prompt structure.

The two constraints that made the biggest difference:

  • "Find the root cause, not the line that threw" (prompt #1)
  • "Trace two concurrent executions step by step" (prompt #7)

Without those explicit instructions, Claude reads code sequentially and misses the real cause almost every time.

Collapse
 
devprompts profile image
Dev Prompts

TL;DR for the prompts:

  1. Stack trace → root cause (not just the line that threw)
  2. Explain WHY this error happens at the mechanism level
  3. Expected vs. actual → find the logic bug
  4. Flaky test → find the nondeterminism source
  5. Regression → which commit caused it and why
  6. Performance bottleneck → specific line, not "add caching"
  7. Async/concurrency → trace two executions step by step
  8. Third-party API error → auth, payload, rate limit, version
  9. Minimal reproduction case
  10. Post-fix → prevent the whole class of bug

The pattern across all 10: context + constraint + format = diagnosis instead of a lecture.