DEV Community

Cover image for My debugging routine, written down so I stop skipping steps
SURAJ NANDAN
SURAJ NANDAN

Posted on

My debugging routine, written down so I stop skipping steps

When something breaks, my worst instinct is to start changing things. This is the checklist I use to override that instinct, illustrated with real bugs from my own projects.

The routine

1. Reproduce it

If I can't make it fail on demand, I'm guessing. Before touching any code, find the smallest sequence of steps that triggers the problem every time. If it's intermittent, find what makes it more likely (timing, data, load).

2. Read the actual error

All of it, including the line I'd normally scroll past. Error messages are often precise. The half I skip is usually the half that names the culprit.

3. Shrink the problem

Remove code until the bug disappears. The last thing removed is the prime suspect. This is slow and boring and it works better than staring.

4. Form one hypothesis and test it

"I think X is happening because Y." Then change one thing to check. If you can't state the hypothesis, you're not debugging, you're shuffling.

5. Check the boring stuff

Environment variables, wrong branch, stale build, cached response, a service that isn't running. Boring causes are far more common than exotic ones.

6. Explain it aloud

Rubber-duck it, literally. Half of bugs dissolve mid-sentence.

7. Write down the cause

Future me will meet this bug again, or a cousin of it.

Real examples

Here's what the routine looks like applied to bugs from my own project write-ups.

The silent failure (step 5: boring causes)

On Day 3 I wrote about deploys that failed for two weeks. The cause was a boring environment difference: Vercel skipped a postinstall script that my laptop ran. If I'd started with "what is different between my machine and the host?", I'd have found it sooner.

The TTS that never spoke (step 1 and 5)

In Knot.ai, the voice engine's text-to-speech fails silently: the agent joins, reports "running", and never publishes audio. A silent failure has no error to read. The fix was to create the signal: a /health/tts pre-flight that names the missing credentials, and an 18-second audio watchdog in the browser. Lesson: when a system fails silently, add observability before you add guesses.

The detector that flagged everything (step 4: one hypothesis)

Knot.ai's contradiction detector fired on any shared long word, so by round three it flagged everything. The hypothesis was "the match rule is too loose." The fix: tighten it to a real self-retraction, an "I/we" subject plus topical overlap right after a negation. One rule, one change, one re-test.

The spinner that never stopped (step 3: shrink)

In Whiteboard.ai, viewers sat on "Saving…" forever. The question that cracks it is a shrinking one: what writes to the document when a viewer merely opens it? The answer: the store stamped a schema version at construction, an update the server correctly rejects for viewers. Moving the stamp to the first local edit fixed it.

The recursion in the policies (step 2: read the error)

In CardBridge, two Row Level Security policies referenced each other's tables and Postgres reported infinite recursion. The error literally named the problem. Breaking the cycle with SECURITY DEFINER helper functions fixed it.

The rule behind the rules

Change one thing at a time.

Bugs get worse fastest when you change three things at once, because then you can't tell which change helped, which hurt, and which did nothing.

A debugging log template

Keep a tiny file open while you work:

BUG: viewers stuck on "Saving..."
REPRO: open a board as a viewer, wait 5 s
HYPOTHESIS 1: server rejects an update we send  -> CHECK: log rejected frames
RESULT: yes, schema version stamp on construct
FIX: stamp on first local edit
CAUSE (for next time): clients that cannot write must not write on open
Enter fullscreen mode Exit fullscreen mode

It takes two minutes and turns a frustrating hour into a recorded lesson.

When to stop and ask for help

  • You've been stuck for 30–45 minutes without a new hypothesis.
  • You can explain the problem but not why your hypotheses failed.
  • The bug might be in code you don't own.

Asking early is cheaper than being heroic. Write the question as if the other person has no context: what you expected, what happened, and what you've already tried.

git bisect: debugging with history

When something worked last week and doesn't now, let git find the culprit. Bisect does a binary search over commits.

git bisect start
git bisect bad                 # the current commit is broken
git bisect good v1.2.0         # a commit (or tag) you know worked

# git checks out a commit halfway between. Test it, then tell git:
git bisect good                # or: git bisect bad
# ...repeat; after a handful of steps, git names the first bad commit.

git bisect reset               # return to where you started
Enter fullscreen mode Exit fullscreen mode

With 100 commits in range, you need at most about seven steps. You can even automate it with a script that exits 0 for good and non-zero for bad:

git bisect run npm test
Enter fullscreen mode Exit fullscreen mode

Logging that actually helps

console.log("here") is the debugging equivalent of shouting into a room. Better habits:

  • Log values with labels: console.log('claim input', { riderId, triggerType }).
  • Log at boundaries: where data enters and leaves your function.
  • Use levels: debug for noise, info for events, warn and error for problems.
  • Never log secrets or personal data.
  • Remove or lower the level of temporary logs afterwards.

Debugging local versus production

Production bugs are harder because you can't poke at them freely. Some principles:

  • Reproduce locally first, with production-like data and settings.
  • Add observability before guessing: a health check, a log line, a metric. (The silent TTS failure in Knot.ai needed exactly this.)
  • Compare environments: versions, env vars, regions, install policies.
  • Change one thing, deploy, check: with small deploys, a regression points at a small diff.
  • Prefer reversible fixes: feature flags and quick rollbacks are better than heroic patches.

The rubber duck, made practical

When you explain a bug aloud, force yourself to answer three questions:

  1. What did I expect to happen?
  2. What actually happened?
  3. What's the first point where those two diverge?

That third question is the one that finds bugs. Writing the answer as an actual message to a teammate (even one you never send) is a powerful version of the duck.

Common categories of boring causes

Category Examples
Environment Wrong Node version, missing env var, different OS
State Stale cache, old build artifact, leftover data
Timing Race condition, slow network, timeout too short
Permissions Missing policy, wrong key, CORS
Data Null where a value was assumed, unexpected format, timezone
Assumptions "This can never be empty", "this runs once"

When stuck, walk down this table and ask whether each could apply.

After the fix

Add a test that would have caught it, or at least a comment explaining the trap. A bug you fix twice has taught you nothing the first time.

Key takeaways from day 13

What's step zero in your routine? And what's the most embarrassing root cause you've ever found?

Top comments (0)