Every developer has stared at a bug that makes no sense. Early on, my instinct was to change things until it worked. These days I treat a bug like a case to solve: collect evidence, question what I think I know, and narrow it down until there is only one explanation left.
The detective mindset
- Evidence before theories. Read the error, the logs and the network calls before deciding what is wrong.
- Question every assumption. The bug usually lives in the part you were sure about.
- Follow the data. Track a value from where it enters the system to where it goes wrong.
- Write it down. Notes stop you from testing the same idea twice.
1. Reproduce it reliably
A bug you cannot reproduce is a bug you cannot confirm is fixed. I try to find the exact steps, data and environment that trigger it, then shrink that down to the smallest case possible. Bugs that happen only sometimes are often about timing, so I look closely at anything async.
2. Narrow it down
Once I can reproduce it, I split the problem in half. Add a checkpoint in the middle of the flow and see whether the data is still correct there. If it is, the bug is later. If not, it is earlier. A few rounds of this and the problem has nowhere to hide.
async function createOrder(input) {
console.log("1. input", input);
const cart = await getCart(input.cartId);
console.log("2. cart", cart);
const total = calculateTotal(cart.items);
console.log("3. total", total);
return saveOrder({ ...input, total });
}The first checkpoint that shows the wrong value tells you exactly where to look. For regressions, git bisect does the same thing across commits.
3. Check your assumptions
Most of my worst bugs came from something I assumed was always true. The API always returns a list. The date is always valid. The user always has a profile. Then one day it does not.
// Assumes the API always returns data
const names = response.data.users.map((u) => u.name);
// Handles the day it does not
const names = (response.data?.users ?? []).map((u) => u.name);4. Know your tools
- Console:
console.tablefor arrays,console.timefor slow code,console.traceto see who called what. - Network tab: check the request payload, the response and the status code before blaming the frontend.
- Breakpoints: a
debuggerstatement lets you pause and inspect state instead of guessing. - React DevTools: see props, state and why a component rendered.
5. Form a hypothesis and test it
"The API is broken" is not a hypothesis. "The API returns an empty list when the filter has a trailing space" is. A specific idea can be proven or disproven in minutes, often with a small failing test that later becomes a regression test.
Patterns I see again and again
- Race conditions: a promise read before it resolves, or two requests finishing in the wrong order.
- Off by one: a loop or slice that misses the first or last item.
- Mutated state: changing an object in place so React never notices it changed.
- Environment drift: something that works locally because of a missing variable or different data in production.
A quick checklist
- Can I reproduce it every time?
- Have I read the full error and stack trace?
- What did I assume that I have not verified?
- Did I check the network tab and the logs?
- Has someone already hit this exact issue?
- Have I taken a short break and come back fresh?
Wrapping up
Debugging is a skill you build by doing it on purpose. Slow down, collect evidence and let the facts lead. And every bug you solve teaches you how to write code that is a little harder to break next time.