Module 7 · Error Handling, Debugging & Testing ⏱ 19 min

Debugging Strategies

By the end of this lesson you will be able to:
  • Reproduce a bug reliably and read its error message and stack trace
  • Use logging and assertions to inspect state and surface broken assumptions
  • Apply divide-and-conquer to isolate a defect instead of editing blindly

No one writes bug-free code, and that is not the goal. The skill that separates experienced programmers from beginners isn't writing perfect programs — it's finding the inevitable mistakes quickly. A bug you can locate in two minutes costs nothing; the same bug hunted for two hours ruins your afternoon.

This lesson is about strategy: a repeatable process for turning 'it doesn't work' into 'I know exactly which line is wrong and why'. The good news is that debugging is a craft, not luck. Reproduce, observe, hypothesise, test, and narrow — those five moves, applied patiently, dissolve almost every defect. We'll build them up one at a time.

flowchart LR
  O["observe the bug"] --> H["form a hypothesis"]
  H --> T["test it<br/>log or assert"]
  T --> Q{"fixed?"}
  Q -- No --> H
  Q -- Yes --> D["done"]
  style O fill:#3776ab,color:#fff
  style D fill:#1e7f3d,color:#fff
Debugging is a loop: observe, hypothesise, test, and repeat — never random edits.

Step one: reproduce, then read

Before you can fix a bug you must be able to reproduce it — to trigger it on demand with a specific sequence of inputs. A bug that appears 'sometimes' is one you can never verify a fix for, so pin down the exact steps first. Once it reproduces reliably, slow down and read the error message: it almost always names the file, the line number, and what went wrong. Beginners skim past error text; experienced developers read every word, because the message is the program telling you exactly what failed and where.

Logging at the boundaries turns invisible state into something you can read. Press Run.
function process(items) {
  console.log("entered process with", items);
  let total = 0;
  for (let i = 0; i < items.length; i++) {
    console.log("  i=" + i + " item=" + items[i]);
    total += items[i];
  }
  console.log("total before return:", total);
  return total;
}
console.log("result:", process([10, 20, 30]));

Log to see what your code actually does

Your mental model of what the code does and what it really does are two different things, and the bug lives in the gap. Logging is how you close that gap: drop console.log statements at the boundaries — function entry, loop iterations, just before a return — and watch the values flow past. The wrong value, the extra loop trip, the undefined you didn't expect: once you can see it, you can fix it.

Aim for a few well-placed logs over a wall of them. Log the inputs, the key intermediate result, and the output; if those three look right, the bug is somewhere you haven't logged yet. Each log narrows the search.

flowchart TD
  E["Error: x is undefined<br/>at stepB (line 4)<br/>at stepA (line 2)<br/>at main (line 9)"] --> N["newest call on top"]
  N --> R["the error happened in stepB, line 4"]
  style E fill:#b91c1c,color:#fff
  style R fill:#1e7f3d,color:#fff
A stack trace lists calls newest-first. Read the top frame to find where the error originated.

Read the stack, newest first

When an error crashes your program, the stack trace printed beneath it is a map of how you got there. It lists the call stack with the most recent call on top: the first line is where the error actually happened, and each line below is the function that called it, walking back to where the run began. Read it top-down to find the exact origin, then look at the lines beneath to understand the path that led there. A stack trace turns 'something blew up' into 'this specific line, reached via these specific calls'.

State your assumptions with assertions

Much of debugging is discovering that an assumption you held quietly ('this number is always positive', 'this array is never empty') was wrong. An assertion turns that hidden assumption into an explicit check that fails loudly the moment it's violated, instead of letting the bad value flow on and cause a confusing symptom elsewhere.

You can build a tiny assert helper yourself: if the condition is false, throw an Error naming the assumption. Sprinkle assertions at the boundaries where data enters a function, and the instant a caller hands you something impossible, the program stops right there — at the cause — rather than five functions downstream at the symptom.

A tiny assert helper makes a broken assumption stop the program at the cause. Press Run.
function assert(condition, message) {
  if (!condition) {
    throw new Error("assertion failed: " + message);
  }
}
function divide(a, b) {
  assert(b !== 0, "divisor must not be zero");
  return a / b;
}
console.log(divide(10, 2));
try {
  divide(10, 0);
} catch (e) {
  console.log(e.message);
}
flowchart TD
  S["suspicious range<br/>20 lines"] --> M["log or assert in the middle"]
  M --> Q{"bug before or after?"}
  Q -- before --> H1["keep the first half"]
  Q -- after --> H2["keep the second half"]
  H1 --> M
  H2 --> M
  style M fill:#b45309,color:#fff
Bisect: test in the middle of the suspect range, keep the half that contains the bug, repeat.

Divide and conquer

When the bug could be anywhere in a long stretch of code, don't read all of it hoping to spot the mistake. Bisect instead: put a log or an assertion in the middle of the suspicious range. If the value is already wrong there, the bug is in the first half; if it's still right, the bug is in the second half. Keep the half that contains the bug and repeat. Each test cuts the search in half, so twenty suspicious lines shrink to one or two in four checks.

This is the same idea behind a binary search, and it works because you've turned a guessing game into a series of decisive yes-or-no questions. Name a hypothesis ('the total is wrong by the time we reach line 12'), test it once, and let the answer halve your search.

Logging each stage of a pipeline isolates the culprit: subtotal is 13, not 30. Press Run.
const price = 10;
const qty = 3;
const subtotal = price + qty;   // suspicious: looks like it should multiply
const total = subtotal * 2;
console.log("subtotal:", subtotal);
console.log("total:", total);
Exercise

You see a bug report: 'the total is sometimes wrong.' Before changing any code, what should you do FIRST?

Exercise

What does this print? The comment says it should sum 1 through 5 to 15. Predict the actual output — the gap between 15 and what you see is the bug.

// meant to sum 1 + 2 + 3 + 4 + 5 = 15
function sumTo(n) {
  let total = 0;
  for (let i = 0; i < n; i++) {
    total += i;
  }
  return total;
}
console.log(sumTo(5));
Exercise

Write assertEqual(actual, expected) that throws new Error("got X expected Y") (with X and Y filled in) when the two values are not === equal, and returns true when they are.

function assertEqual(actual, expected) {
  // throw if actual !== expected; otherwise return true
}
Exercise

This average returns NaN for perfectly good arrays. The loop bound is wrong — it walks one step past the end and reads undefined. Fix the one comparison so it sums exactly the elements that exist.

function average(nums) {
  let sum = 0;
  for (let i = 0; i <= nums.length; i++) {
    sum += nums[i];
  }
  return sum / nums.length;
}
Exercise

A form submits and the server saves an empty name. A developer 'fixes' it by making the server default empty names to 'Anonymous'. What is wrong with this fix?

Recap

  • The first move in any bug is to reproduce it reliably and then read the error message — it usually names the file, line, and what went wrong.
  • Log intermediate values to trace what your code actually does versus what you assumed.
  • Assertions turn hidden assumptions into loud failures: state the invariant, fail immediately when it breaks.
  • Divide and conquer: test in the middle of the suspect range and keep the half that contains the bug — each test halves the search.
  • Debugging is a loop: observe, hypothesise, test, repeat. Never edit blindly, and fix the root cause, not the symptom.

Next you'll turn these one-off checks into a permanent safety net with unit tests.

Checkpoint quiz

A program crashes and prints a stack trace. Where do you look first to find the cause?

You suspect a bug somewhere in a 30-line function. What is the fastest way to narrow down where it is?

Go deeper — technical resources