Bugs and Debugging: Finding the Cause Behind Wrong Behavior
📑 On this page
- Bugs come from more than typing mistakes
- A concrete example: the empty cart
- First make the failure reproducible
- Reduce the problem
- Observe before changing
- Form a hypothesis
- Use binary search on changes
- Understand the difference between cause and trigger
- Make the smallest complete fix
- Verify and prevent regression
- Common debugging traps
- Debugging in production
- Knowledge check
- The one idea to remember
Software can run without crashing and still be wrong. A checkout may calculate the wrong tax, a search may omit valid results, or a notification may be sent twice.
A bug is a mismatch between intended and actual behavior. Debugging is the disciplined process of finding why that mismatch occurs.
The visible failure is a symptom. The code line that finally produces it may be far from the underlying cause.
Bugs come from more than typing mistakes
A syntax mistake can prevent code from running, but many bugs are valid instructions built on a false assumption.
Common sources include:
- Incorrect logic
- Unexpected input
- Missing or stale data
- Configuration differences
- Timing and concurrency
- Network failures
- Resource limits
- Misunderstood requirements
- Interaction between individually correct components
Sometimes the program follows its instructions perfectly. The instructions themselves do not match what users need.
A concrete example: the empty cart
Suppose an order total works for normal carts but displays NaN for an empty cart.
Randomly rewriting the calculation may hide the symptom or create another bug. A stronger investigation begins with a reproducible case:
- Create an empty cart.
- Run the same action.
- Confirm the failure occurs consistently.
- Inspect the values entering the calculation.
- Trace the first point where reality differs from expectation.
Perhaps the code expects discount.amount, but an empty cart has no discount object. The final broken total is downstream evidence; the missing value is closer to the cause.
First make the failure reproducible
A reproducible bug turns an anecdote into an experiment.
Record:
- Exact steps
- Input data
- Expected result
- Actual result
- Software version
- Browser, operating system, or device
- Time and relevant identifiers
- Whether the issue is consistent or intermittent
Intermittent failures are harder, but the same principle applies. Capture logs, timing, load, and environment until a pattern appears.
Reduce the problem
Large systems contain too many possible causes to inspect all at once. Debugging narrows the search.
Useful questions include:
- Does it happen for every user or one account?
- Every input or only empty, large, or unusual values?
- Production only, or locally too?
- Before or after a recent release?
- In the user interface, service, database, or network?
A minimal reproduction removes unrelated pieces while preserving the failure. If ten steps can be reduced to two, the remaining code becomes much easier to reason about.
Observe before changing
Evidence can come from:
- Error messages and stack traces
- Structured logs
- Debugger breakpoints
- Variable inspection
- Network request traces
- Database queries
- Metrics and distributed traces
- Screenshots or recordings
Adding a log is useful when it tests a specific question:
logger.info("calculating total", {
itemCount: cart.items.length,
hasDiscount: Boolean(cart.discount),
});Dumping every value creates noise and may leak sensitive data. Good observability records enough context to distinguish hypotheses while respecting security and privacy.
Form a hypothesis
A hypothesis is a testable explanation:
The total fails because the empty-cart response omits the discount object, but the calculator reads it unconditionally.
That statement suggests an experiment: inspect the response and invoke the calculator with and without the object.
Compare this with "something is wrong in checkout." The vague statement provides no next step.
Debugging often repeats a loop:
- Observe.
- Propose a cause.
- Predict what would be true if it were correct.
- Run a focused experiment.
- Keep or reject the hypothesis.
Use binary search on changes
When a bug appeared recently, developers can search version history. Test a revision known to work and one known to fail, then check a revision halfway between them.
Repeatedly halving the range identifies the first bad change much faster than testing every commit. Git's bisect command automates this idea.
The responsible conclusion is not always "that commit is bad." A change may expose an older hidden assumption. The first failing revision is evidence about where behavior changed.
Understand the difference between cause and trigger
A trigger makes the failure visible. A cause makes the system vulnerable to it.
For example:
- Trigger: two requests arrive almost simultaneously.
- Cause: both update shared state without coordination.
Blocking the second request may reduce incidents, but fixing the race condition addresses the underlying defect.
The root cause is the deepest actionable explanation that, when corrected, prevents the class of failure without merely hiding its current symptom.
Make the smallest complete fix
A good correction:
- Handles the failing case
- Preserves correct existing behavior
- Fits the intended requirements
- Does not merely suppress the error
- Includes appropriate validation or error handling
For the empty-cart example, the right fix might be to define a guaranteed response shape, default a missing discount to zero, or reject invalid data at a system boundary. The choice depends on the contract, not only the easiest line to change.
Verify and prevent regression
After changing code:
- Repeat the original reproduction.
- Test nearby edge cases.
- Run the broader test suite.
- Add an automated test that failed before and passes now.
- Check production-like configuration when relevant.
The regression test preserves what the investigation learned:
test("an empty cart has a zero total", () => {
expect(calculateCartTotal({ items: [] })).toBe(0);
});A passing test does not prove the whole system is correct, but it guards this specific contract.
Common debugging traps
Avoid:
- Changing several variables at once
- Trusting memory instead of reproducing
- Assuming the newest-looking error is the original cause
- Ignoring data and configuration
- Fixing only the visible symptom
- Declaring success without rerunning the failing case
- Blaming a person instead of improving the system
Pressure encourages random edits. A written timeline and explicit hypotheses restore discipline.
Debugging in production
Production systems require extra care. Real users and real data are involved.
Developers may use feature flags, canary releases, read-only inspection, traffic replay with sanitized data, or temporary targeted logging. Any diagnostic change should have a clear scope, owner, and removal plan.
Sensitive values such as passwords, tokens, personal records, and payment data should never be casually logged.
Knowledge check
- Why is a visible symptom not necessarily the root cause?
- What information makes a bug reproducible?
- How does a minimal reproduction help?
- What makes a debugging hypothesis useful?
- Why should a fix include a regression test?
The one idea to remember
Debugging is evidence-driven investigation: reproduce the failure, narrow the possibilities, test a specific explanation, correct the underlying cause, and verify that the behavior stays fixed.