Skip to content

P2.4 — Errors, Bugs & the Debugging Mindset

What you'll learn

  • That errors are normal and are information, not failure.
  • The two kinds of problem: an error that stops the program, and a logic bug that runs but is wrong.
  • How to read an error message for where and what.
  • The debugging loop: observe → guess the cause → test the guess — and print as your main tool.

How it applies

  • Reading errors is the actual skill. Every programmer, at every level, produces errors constantly. The difference between fast and stuck is not avoiding errors — it is reading them. An error message names where it happened and what went wrong; that is a head start, not a scolding.
  • Logic bugs hide because nothing complains. The worst bugs run without any error and just produce the wrong answer. There is no message pointing at them, so you find them by observing the program's actual values — which is what print is for.
  • Guessing randomly wastes time. Changing things at random sometimes works and teaches nothing. A deliberate loop — see the symptom, form one guess, test just that — finds the cause and builds understanding. (For a tester, this is the familiar reproduce → narrow → fix.)
  • print turns "I think" into "I see." Most debugging is discovering that a value is not what you assumed. Printing the value at the right spot replaces a guess with a fact, which is usually the whole fix.

Concepts

Errors are information

Code breaks. It breaks for everyone, all day. An error is the computer telling you it could not do what the code asked — and, crucially, telling you where and what. That is help, not failure. The goal is not to write code that never errors (impossible); it is to read the error and respond. A beginner who treats errors as normal feedback progresses; one who treats each as a personal failure stalls.

Two kinds of problem

  • An error stops the program and shows a message. The computer literally cannot continue — a misspelled name, text used as a number (P1.3), an index past the end of a list (P2.2). These are the easy ones, because the program tells you something is wrong and roughly where.
  • A logic bug is worse precisely because it does not error. The program runs to the end and produces a wrong result — a score that is off, a loop that misses the last item, an if that takes the wrong branch. Nothing complains; the answer is simply incorrect. You catch these by checking the program's actual output and values against what you expected.

Reading an error message

An error message has two parts worth finding immediately: where (a file and line number) and what (a short description). For example:

Invalid operands 'String' and 'int' in operator '+'.   at line 7

What: you tried to + a String and an int (the P1.3 trap). Where: line 7. You do not need to understand every word — find the line, look at what it is combining, and the description usually points straight at it. The named line is where the computer noticed the problem; sometimes the real cause is a line or two earlier (a variable set wrong before it was used), so read around it.

The debugging loop

When something is wrong — error or logic bug — work the loop instead of flailing:

  1. Observe. What actually happened? Read the error, or print the values to see the real state. Get the symptom precise: "the total is 90 but should be 100," not "it's broken."
  2. Guess one cause. Form a single, specific hypothesis: "the loop stops one item early," or "this variable is text, not a number."
  3. Test just that. Make one change (or add one print) that confirms or kills the guess. If it was right, you have the fix; if not, you have learned something and you guess again — now better informed.

One change at a time is the discipline. Change five things and something works, and you have no idea which — and no understanding gained.

Example

A logic bug, found by the loop — no error, just a wrong answer:

func _ready():
    var total := 0
    var prices := [10, 20, 30]
    for price in prices:
        total = total - price     # BUG: should be +, not -
    print(total)                  # prints -60, expected 60

Observe: it prints -60; you expected 60. No error fired — the code is valid, just wrong. Guess: the total is going down, so maybe the combining step is subtracting. Test: add print(total) inside the loop to watch it: it shows -10, -30, -60 — confirmed, it is shrinking each pass. Look at the line: total = total - price. The - should be +. One-character fix, found not by staring but by observing the value at each step. That is print-debugging: you saw the truth instead of assuming it.

Walkthrough

Use your P1.1 script setup.

  1. Cause an error on purpose: print("Score: " + 100) (text + number, P1.3). Run, read the message — find the what (operands) and the where (line). Fix it with str(100).
  2. Cause a "name not found" error: print a variable you never created (a typo like scoer). Read how the message names the unknown identifier and the line. Fix the spelling.
  3. Reproduce the logic-bug example (the - that should be +). Run, see -60, then add a print inside the loop to watch total change. Use what you see to locate and fix the wrong operator.
  4. Practice the loop out loud on that bug: state the observation ("−60, expected 60"), your one guess ("it's subtracting"), and the one test that confirmed it (the in-loop print). Naming the steps is the habit.

Optional sanity check

Take any small program you wrote earlier and add a print that reports a value you think you know — a loop counter, a variable mid-calculation. Often it is exactly what you expected, which builds trust; occasionally it is not, and you have just found a bug you did not know you had. Printing what you assume is how assumptions get checked.

Self-check quiz

Q1 — The program stops and shows an error message. What is the first thing to do?

A. Change random lines until it works. B. Read the message for what went wrong and where (the line), then look at that line. C. Restart the computer. D. Delete the file and start over.

Reveal answer

B. An error message names the problem and the line — read it first; it is the fastest route to the cause. A is flailing (works sometimes, teaches nothing). C and D are over-reactions that discard information instead of using it.

Q2 — A program runs with no error but prints the wrong number. What kind of problem is this, and how do you find it?

A. An error; the message will point to it. B. A logic bug; since nothing complains, you find it by printing the actual values to see where they diverge from what you expected. C. Impossible — wrong output always causes an error. D. A hardware fault.

Reveal answer

B. Wrong-but-runs is a logic bug; there is no message, so you observe the real values (print-debugging) and compare to expectations to locate the divergence. A is wrong — logic bugs produce no error. C is false (most wrong output never errors). D misattributes a code bug to hardware.

Q3 — Why change only one thing at a time when debugging?

A. It is faster to change many. B. So that when the behavior changes, you know exactly which change caused it — preserving understanding. C. The editor only allows one change. D. It does not matter.

Reveal answer

B. A single change isolates cause and effect: if it fixes (or moves) the problem, you know why. Changing many at once can stumble into a fix but leaves you unable to say which change mattered — no understanding gained. A trades understanding for speed and usually costs more. C is false; D dismisses the core discipline.

Integration question

Q4 — open

A function is supposed to return the largest of two numbers, but max_of(3, 9) returns 3. The code is func max_of(a, b): if a > b: return a (and nothing else). Walk the debugging loop on this: what you observe, one specific guess, the one test that confirms it, and the fix — and say whether this is an error or a logic bug, and how you can tell.

Reveal expected answer

Kind: it is a logic bug, not an error — the program runs and returns a value with no message; it is just the wrong value. You can tell because nothing stopped or complained; the output is simply incorrect. Observe: max_of(3, 9) returns 3, but the larger number is 9. Get it precise: it returns the wrong one specifically when the second argument is larger. Guess (one, specific): the function only handles the case where a > b and has no path for when b is larger — so when a > b is false, it falls off the end and returns nothing useful (or a default), and certainly not b. Test: add a print(a, b) and a print("took if branch") inside the if; calling max_of(3, 9) shows the if branch was not taken (since 3 > 9 is false), confirming there is no handling for that case. Fix: add the missing path — else: return b (or an elif/else) so the function returns b when b is the larger:

func max_of(a, b):
    if a > b:
        return a
    else:
        return b

The loop turned "it's broken" into a precise observation, one testable guess (the missing branch), a confirming test (the branch was skipped), and a one-line fix — and recognizing it as a logic bug told you to observe values rather than wait for an error that was never coming.