Engineering

Debugging Gets Easier When You Stop Guessing

Shaquib Imdad
4 min read2 views
Translucent sage glass folds illuminated against a deep green background

You change a condition. Refresh. Still broken. Move a function call. Refresh again. Now the bug has disappeared, but you cannot explain why.

That is an uncomfortable place to stop. The code works in front of you, yet you have little reason to trust the fix.

Debugging becomes more manageable when each change answers a question. Let's work through a small checkout bug and turn a vague report into something we can actually prove.

Make the failure specific

“The total is wrong” is a starting point, not a reproduction.

Suppose a cart contains a notebook priced at 12 and a pen priced at 3. A customer expects a subtotal of 15, but the interface displays 123. Before editing anything, write down those inputs, the expected result, and the observed result.

Keep the reproduction small. Remove discounts, shipping, and unrelated items if the bug still occurs without them. Each detail you can remove is one less thing to investigate.

Also record where the numbers came from. A manually created object and a response from the server may look similar on screen while containing different types of values.

Look at the boundary before the calculation

Here is a plausible implementation:

CODE
function subtotal(items) {
  return items.reduce((total, item) => total + item.price, 0);
}

With numeric prices, this returns the expected result. But consider this input:

CODE
const items = [
  { name: "Notebook", price: 12 },
  { name: "Pen", price: "3" },
];
 
subtotal(items); // "123"

The first addition produces the number 12. The second combines that number with a string, so JavaScript concatenates them.

A log of the final total tells us that something went wrong. A log at the input boundary helps explain what happened:

CODE
console.table(
  items.map((item) => ({
    price: item.price,
    type: typeof item.price,
  }))
);

For this local reproduction, the price and its type are enough. There is no need to dump an entire customer record into a log.

Turn your suspicion into a prediction

The useful hypothesis is precise: “A string price changes addition into concatenation.” It makes a prediction. If the second price is the number 3, the result should be 15. If it is the string "3", the result should be "123".

Check both cases before fixing the implementation. If they behave as predicted, you have evidence for the explanation. If they do not, investigate further instead of piling another change on top.

This is also why changing three things at once is expensive. Even if the bug disappears, you still have to discover which change mattered.

Decide what the function should accept

Wrapping every price in Number() looks convenient, but it introduces a policy you may not intend. For example, an empty string becomes zero. A missing price should not quietly become a free item.

For this example, let's make the contract strict: prices must already be finite, nonnegative numbers. Parse external formats in a separate boundary function where their rules are explicit.

CODE
function subtotal(items) {
  return items.reduce((total, item) => {
    const price = item.price;
 
    if (
      typeof price !== "number" ||
      !Number.isFinite(price) ||
      price < 0
    ) {
      throw new TypeError("Price must be a finite, nonnegative number");
    }
 
    return total + price;
  }, 0);
}

This prevents the silent concatenation. It does not repair the upstream data on its own: the caller still needs to supply valid prices, and the application needs to handle invalid data appropriately.

The whole-number prices here keep the example focused on types. A real checkout also needs a deliberate currency representation and rounding policy; this function is not a complete money library.

Verify the explanation, then the fix

Run the original two-item case with valid numeric prices. Confirm that it returns 15. Then verify that the malformed string price is rejected instead of producing a believable-looking total. Check an empty cart too, because its expected subtotal should be explicit.

Finally, repeat the original path through the application. A helper can pass its checks while the page still receives the wrong data or displays an old value.

Before closing the bug, write a short explanation: what input triggered it, why the old code failed, and what now prevents the failure. If that explanation is difficult to write, there may still be a gap in the investigation.

The next time a refresh seems to “fix” something, pause before moving on. Try to predict exactly which input will break it again. That prediction is often the first useful step toward a fix you can trust.

KEEP THE THREAD GOING

Up next.