DEV Community

Daniil Kornilov
Daniil Kornilov

Posted on Edited on

A practical debugging checklist: reproduction, bisection and inspection

Disclosure: This article was created or edited with AI assistance. Revised on 13 October 2026 to remove promotional links and unsupported outcome claims. It is educational guidance, not a verified personal case study or a measured result. Examples are illustrative; use only your own documented experience and measurements.

The sections below present options and illustrative examples. Adapt them to the requirements of your project or role; they do not establish a universal rule or promise a result.

Technique 1: Rubber Duck Debugging (But Actually Do It)

Yeah, you've heard of this one. Explain your code to a rubber duck. Cool concept, nobody actually does it.

"OK so this function takes a user ID, queries the database for their orders, filters out cancelled ones, and returns the total."

Writing the expected behavior can expose an assumption that needs checking.

Here's the thing that makes this work: you have to be specific. Don't say "this function processes the data." Say exactly what it does with the data. The bug lives in the gap between what you think the code does and what it actually does.

Pro tip: If you're working remotely and feel weird talking to yourself, open a DM with yourself on Slack. Type your explanation there. Same effect, and it creates a searchable log of your debugging process.

Technique 2: The Binary Search (Bisect) Method

Bisection is useful when you can reliably classify a point as working or broken.

When you have a bug and you don't know where it is, most people start from the beginning and trace through the code linearly. That's O(n) debugging. We can do better.

The bisect method works like this:

  1. Find a point in your code where you know things are correct (usually the input)
  2. Find a point where things are broken (usually the output)
  3. Check the midpoint
  4. If the midpoint is correct, the bug is in the second half
  5. If the midpoint is broken, the bug is in the first half
  6. Repeat

This turns an O(n) search into O(log n). For a 1000-line function (yes, they exist, no, they shouldn't), that's the difference between checking 1000 spots and checking 10.

Git bisect uses this exact same principle. If you know your code worked in commit A and is broken in commit Z, git bisect will binary search through your commit history to find exactly which commit introduced the bug.

git bisect start
git bisect bad          # current commit is broken
git bisect good abc123  # this old commit worked fine
# you test it
git bisect good  # or git bisect bad
# repeat until found
Enter fullscreen mode Exit fullscreen mode

Technique 3: Print Debugging Done Right

"Just use a debugger" is advice that sounds smart but ignores reality. Sometimes you're debugging a production system. Sometimes the bug disappears when you attach a debugger (hello, race conditions). Sometimes you're working in an environment where breakpoints aren't practical.

Print debugging isn't bad. Bad print debugging is bad.

Here's how to do it right:

Label everything. Not print(x). Instead: print(f"[DEBUG] user_id={user_id}, order_count={len(orders)}, total={total}"). When you have 15 print statements and they all just say random numbers, you've created a new problem.

Add timestamps. If you're debugging anything async or performance-related, timestamps are essential.

import time
print(f"[{time.time():.3f}] Starting order processing for user {user_id}")
Enter fullscreen mode Exit fullscreen mode

Use conditional prints. Don't print every iteration of a loop that runs 10,000 times. Print when something unexpected happens.

for order in orders:
    if order.total < 0:  # this shouldn't happen
        print(f"[WARN] Negative total found: order_id={order.id}, total={order.total}")
Enter fullscreen mode Exit fullscreen mode

Clean up after yourself. I use a specific tag like #DEBUG so I can search and delete all my debug prints when I'm done. Or use logging levels so debug prints don't leak into production.

Technique 4: Breakpoints Over Print (When Possible)

OK so I just defended print debugging, but let me be real: when you can use breakpoints, they're almost always better.

The main advantage isn't just seeing variable values. It's being able to explore. When you hit a breakpoint, you can:

  • Check any variable in the current scope
  • Evaluate expressions on the fly
  • Step through code one line at a time
  • Move up and down the call stack
  • Modify variables and continue execution

That last one is huge. You can test a fix without restarting your program. "What if this value was 5 instead of 0?" Just change it in the debugger and see what happens.

Most modern IDEs make this dead simple. VS Code, PyCharm, Xcode, they all have excellent debugging tools. Set aside time to learn the debugger for your IDE and language.

Conditional breakpoints are the secret weapon. Instead of breaking every time a line is hit, only break when a condition is true. Like only pause when user_id == 42 or when response.status >= 400. This saves so much time when debugging issues that only happen with specific data.

Technique 5: Minimize the Reproduction

This is probably the most important technique and the one most people skip.

When you find a bug, your first instinct is to start fixing it. Fight that instinct. Instead, spend time making the smallest possible reproduction case.

If the bug happens in a 500-line function, can you reproduce it in a 20-line script? If it happens with a specific API request, what's the minimal JSON payload that triggers it?

Why? Because a minimal reproduction:

  1. Often reveals the root cause just by creating it
  2. Makes it obvious when the bug is fixed
  3. Can become a test case
  4. Is way easier to share with teammates

Technique 6: Read the Error Message. No, Actually Read It.

I know this sounds condescending. Bear with me.

When you see a stack trace, what do you do? If you're like most developers, you look at the top or bottom line and start guessing. Stop doing that.

Read the entire error message. Read the stack trace from bottom to top. Understand the chain of calls that led to the error. The actual cause is often several frames deep, not at the top.

Also, google the exact error message. Not a paraphrased version. The exact text. Copy paste it into your search engine. Chances are someone hit this exact same error and posted about it.

Technique 7: Explain It to Someone Else

Different from rubber duck debugging. I mean actually grab a colleague and explain the bug to them.

This works for two reasons. First, the same effect as rubber duck debugging. Explaining forces clarity. Second, fresh eyes. They might immediately spot something you've been staring past for an hour.

Some teams formalize this as "debugging pairs." One person drives, one person asks questions. It works really well.

My Debugging Checklist

When I'm stuck, I literally go through this list:

  1. Read the error message properly
  2. Create a minimal reproduction
  3. Check the obvious things (typos, wrong variable, off-by-one)
  4. Explain the expected vs actual behavior out loud
  5. Binary search for the problem location
  6. Check what changed recently (git diff, git log)
  7. Take a 10-minute break, then look again
  8. Ask someone else to look at it

The Meta-Skill: Knowing When to Stop

The hardest debugging skill isn't technical. It's knowing when you've been staring at something too long and need to step away.

When repeated guesses stop producing evidence, consider a break, a smaller reproduction or a second reviewer.

Set a timer. If you haven't made meaningful progress in 30-45 minutes, get up. Walk around. Work on something else. Come back in an hour.

Wrapping Up

Start with one technique. Try the bisect method on your next bug. See how it feels. Then gradually add others to your toolkit.

Your future self, the one who's not staying late because of a mystery bug, will thank you.

Top comments (0)