3.1Syntax and semantic errors

Last updated June 11, 2026

Chapter introduction

Software errors are easy to make and inevitable: studies of professional codebases regularly count them per hundred lines, written by people doing this for a living. So this course handles debugging the way it handled functions: as a first-class skill with its own chapter, placed early, because you've been debugging informally since your first typo and you'll do it every programming day of your life. This chapter is about doing it on purpose.

Errors come in two broad kinds, and knowing which kind you're facing tells you which tools to reach for.

Syntax errors

A syntax error breaks the language's grammar, and you know these well by now: the missing semicolon of lesson 1.1, the keyword used as a name, the unclosed brace. The compiler catches every single one, points at it, and usually drafts the fix. As irritations go, syntax errors are mosquito-grade: frequent, briefly annoying, never dangerous.

Semantic errors

A semantic error (or logic error) is a program that says something the language allows, but not what you meant. The grammar is fine; the meaning is wrong:

fn add(x: i32, y: i32) -> i32 {
    x - y
}

fn main() {
    println!("{}", add(5, 3));
}
2

It compiles without a murmur. It runs without a complaint. It is also wrong, because that function is add in name and subtraction in fact. The compiler can verify that your program is well-formed, not that it matches the intentions in your head; nothing in the rules of Rust says a function named add must add. (One typo'd operator, and we'll meet this little program twice more this chapter; it's the lab rat.)

Where Rust moves the line

If you've absorbed lesson 1.7's key insight, you can predict this section. In most languages, the semantic-error category is huge and includes the monsters: reading uninitialized variables, using values of the wrong type, accessing memory that's gone. Rust's compiler reclassifies nearly all of those as compile-time errors; you've personally watched it refuse an uninitialized read (lesson 1.4) and a type mismatch (lesson 1.7).

What's left at runtime splits into two camps:

Errors Rust catches as the program runs. Some operations are checked, and failing the check stops the program immediately with a report, called a panic. You've technically been arming panics all along (expect is a panic with your message on it; todo!() is one too). Arithmetic offers the classic example: dividing by a value that arrives, at runtime, as zero. Here the divisor comes from the user, so the compiler has no way to know what's coming:

fn main() {
    println!("Enter a number to divide 8 by:");
    let y = read_number();
    println!("{}", 8 / y);
}

fn read_number() -> i32 {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("failed to read input");
    input.trim().parse().expect("that wasn't a whole number")
}

Feed it a 0 and:

Enter a number to divide 8 by:
0
thread 'main' (1139364) panicked at src/main.rs:4:20:
attempt to divide by zero
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

(The parenthesized number is a thread ID, different on every run; the part worth reading is everything after it.)

The program died, but it died honestly: named cause, file, line, column, and an offer of more detail. Chapter 12 covers panics properly, including when stopping is the right design and when it isn't. For this chapter, a panic is good news wearing bad news's coat: something went wrong and the program told you where.

Errors nothing catches. And then there's our add above: wrong answer, no crash, no message, no line number. Pure logic errors produce output that's merely false, and no compiler in any language can save you, because the program is doing exactly what it says. These are what debugging techniques (and this chapter) are actually for.

For advanced readers

Why did the example above bother asking the user for the zero? Because the compiler refuses anything it can prove will panic. Write the zero as a literal and there's no runtime to wait for:

error: this operation will panic at runtime
 --> src/main.rs:2:20
  |
2 |     println!("{}", 8 / 0);
  |                    ^^^^^ attempt to divide `8_i32` by zero
  |
  = note: `#[deny(unconditional_panic)]` on by default

And its reach is longer than literals: even splitting it into let y = 0; and 8 / y gets refused, because the compiler propagates the constant and proves the panic inevitable. The general principle, which you'll see again and again: anything the compiler can prove broken at compile time, it refuses; the runtime checks exist for what depends on values it can't see. User input is the canonical thing it can't see, which is why our example needed you to type the fatal zero personally.

Key insight

Three tiers, then. Grammar mistakes: compile error. Meaning mistakes the language can define rules for: mostly compile errors in Rust, occasionally runtime panics with an honest report. Meaning mistakes only you could define rules for: silent wrong answers, your job, this chapter. Rust shrinks the third pile dramatically; it cannot make the pile empty.

Quiz time

Question #1

Classify each as a syntax error, a compile-time semantic catch, a runtime panic, or a silent logic error:

a) let x = 5 (no semicolon, mid-function, more code follows) b) A function meant to compute an average that divides by the wrong count, producing plausible-looking numbers c) let x: i32; println!("{x}"); d) Parsing user input of "zero" with the chapter 1 recipe

Show solution

a) Syntax error: the grammar's broken, compile fails. b) Silent logic error: compiles, runs, lies. c) Compile-time semantic catch: use of an uninitialized variable, E0381. d) Runtime panic: parse fails on non-numeric text and expect stops the program with your message.

Question #2

Why can't the compiler catch the add-that-subtracts bug, even in principle?

Show solution

Because nothing in the program is inconsistent. The types check, the syntax is legal, and x - y is a perfectly valid thing for a function to compute. The error exists only in the gap between the code and your intention, and the compiler can't see intentions. (Chapter 14's tests are how you write your intentions down in checkable form.)

Knowing the kinds is the field guide. Next: the actual process of hunting one down, in steps.