Part 2: Core Programming Concepts

9. Errors, Debugging & Testing

Things will go wrong — and that is normal

Every programmer, from day one to decades in, writes code that does not work the first time. The skill that separates the frustrated from the productive is not "never make mistakes" — it is finding and fixing them calmly. That starts with understanding the three kinds of errors.

The three kinds of errors

TypeWhen it happensSymptomExample
Syntax errorBefore the program runsCode won't start; the language can't parse itMissing ), misspelled keyword, bad indentation
Runtime errorWhile the program runsProgram crashes partway with an error messageDivide by zero, index out of range, converting "abc" to a number
Logic errorProgram runs fineNo crash, but the answer is wrongUsing + where you meant -; wrong loop bound

Syntax errors are the friendliest: the computer refuses to run and usually points near the problem. Runtime errors are louder — the program stops midway and prints a message. Logic errors are the sneakiest, because everything looks fine; the program just produces the wrong result, and only testing reveals it.

Reading error messages

Beginners often panic at a wall of red text and ignore it. Do the opposite: read it carefully. An error message typically tells you the type of error, a short description, and the line number where it occurred. Start at the bottom of a stack trace — that is usually closest to your own code.

numbers = [1, 2, 3]
print(numbers[5])
# IndexError: list index out of range   <- type + description
#   line 2, in <module>                 <- where it happened

Debugging strategies

Debugging is detective work: form a theory, test it, narrow down. A few reliable techniques:

  • Print the state. Add print statements to show variable values at key points. Often the value is simply not what you assumed.
  • Read the code aloud. Explaining it line by line (even to a rubber duck) exposes wrong assumptions fast.
  • Isolate the problem. Comment out parts until the error disappears; the last thing you removed is near the cause.
  • Change one thing at a time. If you change five things and it works, you don't know why — and that knowledge is the whole point.
  • Check the boundaries. Off-by-one, empty lists, and the very first/last item cause a huge share of bugs.

Testing: proving it works

Running a program once and seeing it behave is not proof it is correct. Testing means feeding known inputs and checking you get the expected outputs — especially the tricky cases. A simple test is just a comparison you can run again whenever you change the code.

def add(a, b):
    return a + b

# simple checks
print(add(2, 3) == 5)      # True  — normal case
print(add(-1, 1) == 0)     # True  — negatives
print(add(0, 0) == 0)      # True  — edge case

Think about edge cases: empty input, zero, negative numbers, the largest value, the first and last elements. These corners are where bugs hide. Writing tests before you are "done" turns vague confidence into evidence.

Handling errors gracefully

Some errors you can anticipate and recover from rather than crash. Most languages offer a try/catch (in Python, try/except) mechanism so the program can respond to a problem — for example, asking the user to re-enter a number instead of dying.

try:
    age = int(input("Age? "))
    print("Next year:", age + 1)
except ValueError:
    print("That wasn't a number — please try again.")

Key idea: distinguish syntax, runtime and logic errors; read messages instead of fearing them; debug methodically by changing one thing at a time; and test with known inputs and edge cases to turn "it seems to work" into "I can prove it works."