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.
| Command | Does |
|---|---|
cont | run until the next debugger; |
exec <expression> | evaluate anything, in the current scope |
next | run one line |
.exit | quit |
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:
- 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.
- Find the smallest input that still goes wrong. Four numbers, not four hundred.
- Check your assumption in the middle. You believe the loop visits
every item. Print
i, or stop andexecit. One of your beliefs is false, and this is how you find out which. - 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
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.