Contents

Chapter 11

Reading, Logging, and Fixing Bugs

Finding a bug by hand, then with a debugger.

Chapter 10 ended with a crash, and a crash is the friendly kind of bug. It tells you the file, the line, and roughly what went wrong. Here’s the other kind.

function averageOf(numbers: number[]): number {
  let total = 0;
  for (let i = 0; i < numbers.length - 1; i++) {
    total += numbers[i];
  }
  return total / numbers.length;
}

const scores = [10, 20, 30, 40];
console.log(`Average: ${averageOf(scores)}`);
Average: 15

The average of 10, 20, 30 and 40 is 25.

Nothing crashed. tsc said nothing. Every type is correct, every line is legal, and the answer is wrong. No tool is going to tell you about this one, because no tool knows what you meant.

Printing, done properly

The first tool is the one you already have, used deliberately rather than in a panic.

The instinct is to scatter console.log(total) around and squint at a column of bare numbers. Label them instead:

for (let i = 0; i < numbers.length - 1; i++) {
  console.log("i:", i, "adding:", numbers[i], "total so far:", total);
  total += numbers[i];
}
i: 0 adding: 10 total so far: 0
i: 1 adding: 20 total so far: 10
i: 2 adding: 30 total so far: 30
Average: 15

There it is. Three rounds, not four. 40 never got added, and the loop stopped after i: 2.

Two things made that readable. Every value has a name next to it, so you don’t have to remember which column is which. And console.log takes as many values as you like, comma-separated, so one call shows a whole situation instead of one number.

Reading the loop again

With the printout in front of you, go back to the condition:

for (let i = 0; i < numbers.length - 1; i++)

numbers.length is 4. So the loop runs while i < 3, which means i takes the values 0, 1 and 2, and stops. Position 3, holding 40, is never visited.

The - 1 is the bug. It’s there because “the last position is length - 1” is a true sentence you half-remembered from chapter 4, and it belongs in numbers[numbers.length - 1], not in a loop condition.

for (let i = 0; i < numbers.length; i++)
Average: 25

This is the shape almost every bug has: something that is nearly right, applied one place over.

The debugger

Printing works, but you have to guess where to put the prints, and every guess costs an edit and a recompile. A debugger lets you stop the program and look around instead.

Node has one built in. Mark where you want to stop with a debugger; statement:

function averageOf(numbers: number[]): number {
  let total = 0;
  for (let i = 0; i < numbers.length - 1; i++) {
    debugger;
    total += numbers[i];
  }
  return total / numbers.length;
}

debugger; is a real statement, not a comment. When nothing is watching it does nothing at all, so a stray one is harmless. Compile as usual, then run with inspect:

npx tsc average.ts
node inspect average.js
Break on start in average.js:10
  8     return total / numbers.length;
  9 }
>10 const scores = [10, 20, 30, 40];
 11 console.log(`Average: ${averageOf(scores)}`);

It stops before running anything and shows you where it is, with the > marking the current line. Type cont to continue, and it runs until it hits your debugger;:

debug> cont
break in average.js:5
  3     let total = 0;
  4     for (let i = 0; i < numbers.length - 1; i++) {
> 5         debugger;
  6         total += numbers[i];
  7     }

Now you’re stopped inside the loop, and you can ask about anything in scope with exec:

debug> exec total
0
debug> exec i
0
debug> exec numbers.length - 1
3

That last one is the whole investigation. You didn’t add a print, recompile, and re-run. You asked the running program what the loop condition actually comes out as, and it said 3, for an array of four things.

next runs one line and stops again:

debug> next
step in average.js:6
  4     for (let i = 0; i < numbers.length - 1; i++) {
  5         debugger;
> 6         total += numbers[i];
  7     }

.exit leaves. That’s the whole tool for now: cont, exec, next, .exit. Four commands, and they cover most of what you’ll do.

CommandDoes
contrun until the next debugger;
exec <expression>evaluate anything, in the current scope
nextrun one line
.exitquit

Exercise 1 · Ask a question you cannot print

Put a debugger; inside a loop and, when it stops, use exec to evaluate something the program never computes, numbers.length - 1, or total / i, or numbers[i + 1].

That’s the thing printing can’t do. A console.log only shows what you thought to ask for before you ran it. exec lets you ask a question you only thought of once you were already stopped and confused.

How to actually find a bug

The tools matter less than the method, which is the same one every time:

  1. Know what you expected. “Average: 15” is only a bug because you know it should be 25. Work out the right answer by hand first, on input small enough to do that with.
  2. Find the smallest input that still goes wrong. Four numbers, not four hundred.
  3. Check your assumption in the middle. You believe the loop visits every item. Print i, or stop and exec it. One of your beliefs is false, and this is how you find out which.
  4. Fix one thing, then re-run. Same rule chapter 2 gave you for reading error messages, for the same reason: two changes at once and you don’t know which one did it.

Step 3 is the real skill. A bug is always a place where what you believe about the program and what it does have come apart, so debugging is finding which belief is wrong. Everything above is just a way of asking.

Check yourself

1. Your program prints the wrong number, does not crash, and tsc reports nothing. What does that tell you?

Not quite. The types can be entirely correct and the logic still wrong. tsc checks what kind of value you have, never what value it should be.

Yes. This is why the method matters more than the tooling. You have to supply the expected answer yourself before anything can be called wrong.

Not quite. Worth ruling out, but a stale build usually shows old behaviour rather than a plausible wrong number.

2. Why does this chapter mark breakpoints with debugger; rather than a line number?

Not quite. It has one. The problem is which file the line number refers to.

Yes. A debugger; statement lives in the code and moves with it, so it cannot drift. A line number is a guess about a file you did not write.

Not quite. Speed is not the issue. Correctness of the location is.

3. What can exec in a debugger do that a console.log cannot?

Not quite. Both do that perfectly well. The difference is about when you have to decide what to look at.

Yes. A console.log has to be written, compiled and run before it can tell you anything. exec evaluates anything in scope while you are already stopped there.

Not quite. It evaluates expressions in the running program. Fixing the bug still means editing the file.

Project

Hunt the planted bug

Roughly 40 minutes

Type this out exactly as written and save it as stats.ts. It has one bug, and it isn’t the one you just fixed.

function highest(numbers: number[]): number {
  let best = 0;
  for (const n of numbers) {
    if (n > best) {
      best = n;
    }
  }
  return best;
}

function countAbove(numbers: number[], limit: number): number {
  let count = 0;
  for (const n of numbers) {
    if (n > limit) {
      count++;
    }
  }
  return count;
}

const readings = [-5, -12, -3, -20];

console.log("Highest:", highest(readings));
console.log("Above -10:", countAbove(readings, -10));

Work out the right answers first, on paper. The highest of those four numbers, and how many of them are above -10. Write both down before you run anything. You cannot find a wrong answer without a right one to compare against.

Then run it. One of the two lines is right and one is wrong, and the wrong one is wrong in a way that looks completely reasonable.

Find it twice.

First with printing. Put a labelled console.log inside the loop of whichever function is misbehaving, showing every value it considers and what it decides.

Then delete the prints, put a debugger; in the same place, and find it again with cont, exec and next. Use exec on something the program never prints.

The fix is one value, and the interesting part is why it was invisible. Change readings to [5, 12, 3, 20] and the program is perfectly correct. It only fails on data like this, which is the most common shape of a real bug: code that works on every example you thought of while writing it.

Then make it fail honestly. What should highest([]) do, given an empty array? There isn’t a highest of nothing. Whatever you make it do, make it something a reader of the output could not mistake for a real measurement. Chapter 19 is about that question in general.

Stretch: add average(numbers: number[]): number and check it against a total you work out by hand. Then call it on an empty array and look at what comes back, chapter 6 told you exactly what dividing zero by zero gives, and it prints without complaint.