<28/>
28 Lazy Coder
All

The Art of Effective Debugging Or How I Stopped Guessing

Featured Image
The Article

Early in my career, my debugging process was basically: stare at the code, change something that looked suspicious, refresh, and see if it worked. If it didn’t, change something else. Repeat until either the bug fixed itself or I accidentally introduced a second bug that masked the first one.

It worked, technically, in the sense that I eventually shipped things. But it was slow, and worse — I rarely understood why the fix worked. Which meant the same category of bug kept coming back wearing a different disguise.

Debugging well isn’t really about knowing more tricks. It’s a shift from guessing to reading. Here’s the process I actually use now.

Step 1: Reproduce it on purpose

Before touching a single line of code, get the bug to happen reliably, on command. “It broke once and I can’t make it happen again” is the worst possible starting point — you can’t verify a fix for something you can’t reliably trigger.

This sounds obvious, but it’s the step people skip most often, because staring at a stack trace feels more like “doing something” than patiently narrowing down reproduction steps. Resist that urge. Ten minutes here saves an hour later.

Step 2: Read the actual error message. All of it.

Not just the first line. Stack traces read bottom-to-top in most languages, and the first frame that touches your own code (not a library’s internals) is usually where the real story starts. I’ve watched developers scroll straight past the exact line number and file name the error gave them, because the top of the trace was some framework internals that looked scarier than it was.

TypeError: Cannot read properties of undefined (reading 'name')
    at formatUser (utils.js:42)
    at UserCard (UserCard.jsx:18)
    at renderWithHooks (react-dom.development.js:14985)

That trace is telling you, precisely: line 42 of utils.js, something is undefined when you expected it to have a .name property. That’s not a mystery. That’s a treasure map.

Step 3: Form a hypothesis before you change anything

“I think user is undefined because the API call hasn’t resolved yet” is a hypothesis. It’s specific, and it’s falsifiable — you can check whether it’s true. “Let me just try adding a null check” is not a hypothesis, it’s a shot in the dark that happens to also fix the symptom while leaving you no wiser about the cause.

The difference matters because a null check without understanding why the value was null often just moves the bug somewhere less visible.

Step 4: Binary search the problem space

If you genuinely don’t know where in a long function or pipeline something’s going wrong, don’t read every line top to bottom. Add a log statement (or breakpoint) roughly in the middle, check if the data looks right at that point, and narrow down from there — the same way you’d look up a word in a paper dictionary. Each check should eliminate about half of what’s left.

js

console.log('checkpoint A', data); // is data right here?
// ...processing...
console.log('checkpoint B', data); // still right here?

Three or four well-placed checkpoints will usually corner a bug faster than twenty scattered console.logs added in a panic.

Step 5: Check your assumptions, not just your code

A huge share of “impossible” bugs come from an assumption that’s quietly false: the array is never empty (it is, sometimes), this function is always called after that one (it isn’t, under some race condition), the config value is always a string (someone passed a number from a different code path).

When you’re stuck, the fastest unstick move is usually to log the actual shape of your data at the point of failure, rather than the shape you assumed it had. Nine times out of ten, the mismatch between “what I assumed” and “what’s actually there” is the whole bug.

Step 6: Isolate before you fix

If a bug lives inside a big, tangled function, resist fixing it in place first. Copy the smallest possible reproduction into a scratch file or a quick test — just the failing logic, none of the surrounding app. This does two things: it confirms you actually understand the failure (if you can’t reproduce it in isolation, your mental model of the bug is probably wrong), and it gives you a fast feedback loop to verify the fix without reloading your whole app each time.

Step 7: Fix the cause, then write down the symptom

Once you understand why it broke, the fix is usually short. What’s worth the extra two minutes is writing a one-line comment or commit message describing the symptom that led here — not the fix, the symptom. “Fixes crash when a user has no profile photo” is far more useful to future-you searching git blame than “add null check.”

The one habit that changed everything for me

Somewhere along the way I started narrating my debugging out loud — even alone, even if it felt a little silly talking to an empty room. Explaining the problem step by step, as if to a teammate, forces you to actually articulate what you know versus what you’re assuming. Half the time, I catch the bug mid-sentence, before finishing the explanation. This is basically rubber duck debugging, and it’s not a gimmick — it works because bugs love to hide in the gap between what you think you wrote and what you actually wrote, and saying it out loud closes that gap faster than silently staring at a screen ever does.


None of this makes debugging fast, exactly. It makes it predictable — which, after enough late nights chasing a bug that turned out to be a typo three files away, starts to feel like the same thing.

AR

Ashutosh Rajbhar

Full-stack developer writing about clean code, frontend craft, and the occasional debugging war story.

Previous ← Custom WordPress Theme Development: A Complete Beginner’s Guide