Warnings, errors and rustc's suggestions

rustc sorts what it has to say into two groups. A warning means the build succeeded and the binary exists; an error means it did not and the old binary is still in the directory. Both are printed to standard error, both name a file, a line and a column, and both are worth reading before the code is changed.

The warnings in this lesson are the ones a beginner meets first, and the errors are the two that explain most of the rest: a value of the wrong type and a name that is not in scope. Every message below came from this compiler, with its default lint levels, and the lesson ends with the switch that turns warnings into errors on purpose.

Unused names and unused mut

A variable that is never read is a warning, not an error: the code may be a step on the way to something else. The lint is called unused_variables, and the note under it says which lint group it belongs to, unused, which is the name to pass to -A, -W or -D.

A mut that no code needs is a second warning, unused_mut. The two are separate because the fixes differ: one deletes the binding or prefixes it with an underscore, the other deletes one keyword. The program still runs, and the output follows the warnings.

fn main() { let unused = 5; let mut total = 0; println!("total: {}", total); }

rustc --edition 2024 warm.rs
./warm

warning: variable does not need to be mutable
--> warm.rs:3:9
|
3 | let mut total = 0;
| ----^^^^^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default

warning: unused variable: `unused`
--> warm.rs:2:9
|
2 | let unused = 5;
| ^^^^^^ help: if this is intentional, prefix it with an underscore: `_unused`
|
= note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default

warning: 2 warnings emitted

total: 0

The underscore is the documented way to say "I know, and I do not need it": a name that starts with an underscore is not reported, and let _ = expensive() drops the value at once. Renaming to _unused keeps the value alive for the rest of the scope, which is not always what you want.

Unused imports and dead code

An import that nothing uses is unused_imports, and a private function that nothing calls is dead_code. Both are warnings in a binary crate, because both are leftovers that cost the reader time; in a library crate the rule for dead code is different, because anything public may be called from outside.

The example has one of each. Deleting the import and the function is the fix, and cargo suggests the mechanical part: cargo fix removes an unused import on request, which the note at the end of a cargo build mentions.

use std::collections::HashMap; fn unused_helper() -> u32 { 42 } fn main() { let answer = 42; println!("{}", answer); }

rustc --edition 2024 imports.rs
./imports

warning: unused import: `std::collections::HashMap`
--> imports.rs:1:5
|
1 | use std::collections::HashMap;
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default

warning: function `unused_helper` is never used
--> imports.rs:3:4
|
3 | fn unused_helper() -> u32 {
| ^^^^^^^^^^^^^
|
= note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default

warning: 2 warnings emitted

42

The count at the end, 2 warnings emitted, is the line to grep in a long build log. A warning that appears once and then never again is usually a leftover; one that appears in every build is a decision someone has to make.

Unreachable code

Code after a return is never executed, and rustc says so with unreachable_code. The message marks the two places: the statement that always leaves, and the code that can never run. It is a warning because the code still compiles and still means something to somebody, usually a deleted condition or a leftover print.

The same lint fires after a loop without a break, after a panic! and after an expression of type !. Here the return is the plain case, and the program prints only the branch that is reached.

fn main() { let code = 7; if code > 0 { println!("positive"); } else { println!("not positive"); } return; println!("after return"); }

rustc --edition 2024 unreachable.rs
./unreachable

warning: unreachable statement
--> unreachable.rs:9:5
|
8 | return;
| ------ any code following this expression is unreachable
9 | println!("after return");
| ^^^^^^^^^^^^^^^^^^^^^^^^ unreachable statement
|
= note: `#[warn(unreachable_code)]` (part of `#[warn(unused)]`) on by default

warning: 1 warning emitted

positive

A warning does not change the output, which is why the print after return had to be removed before anyone noticed it was dead. A build that treats warnings as errors turns this into a question that has to be answered.

Warnings as errors

--deny warnings promotes every warning to an error, and the build then fails with the same messages. This is what a continuous integration job usually runs, because a warning that nobody reads is worse than a failed build: it trains the team to ignore the output.

The two sections above now stop the build, and each message gains a line that says which flag made it an error and how to allow it locally. #[allow(unused_imports)] on one item is the escape hatch for a case where the warning is wrong, and it should carry a comment that says why.

rustc --edition 2024 --deny warnings imports.rs

error: unused import: `std::collections::HashMap`
--> imports.rs:1:5
|
1 | use std::collections::HashMap;
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `-D unused-imports` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(unused_imports)]`

error: function `unused_helper` is never used
--> imports.rs:3:4
|
3 | fn unused_helper() -> u32 {
| ^^^^^^^^^^^^^
|
= note: `-D dead-code` implied by `-D warnings`
= help: to override `-D warnings` add `#[expect(dead_code)]` or `#[allow(dead_code)]`

error: aborting due to 2 previous errors

The status of that command is 1 and no binary is produced. The same switch works per lint: -D unused_variables fails on that one only, and -W unused_variables raises a lint that is off by default.

A type error, read in full

E0308 is the error for a value whose type is not the one the context needs. The message shows both types and both positions: the annotation, and the value that does not fit. There is no implicit conversion in Rust, so a string literal never becomes a number by itself.

The help lines are the useful part. Here the compiler does not offer a conversion, because there is no general one; the fix is "12".parse().unwrap(), which the lesson on parsing text covers.

fn main() { let count: i32 = "12"; println!("{}", count); }

rustc --edition 2024 mismatch.rs

error[E0308]: mismatched types
--> mismatch.rs:2:22
|
2 | let count: i32 = "12";
| --- ^^^^ expected `i32`, found `&str`
| |
| expected due to this

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0308`.

The last line names rustc --explain E0308, which prints a longer page with examples that compile and examples that do not. The error index is part of the toolchain, so it works on a machine without a browser.

A name that does not exist

E0425 is the error for a name that is not in scope: a typo, a variable used before its let, or a name that belongs to another module. The message points at the use and says only that the name was not found, because the compiler cannot know which of those three happened.

The caret underlines exactly the identifier, which is why the position matters more than the sentence. A missing import produces E0433 instead, and a missing function E0425 as well, with the word function in the message.

fn main() { let total = price + 1; println!("{}", total); }

rustc --edition 2024 missing.rs

error[E0425]: cannot find value `price` in this scope
--> missing.rs:2:17
|
2 | let total = price + 1;
| ^^^^^ not found in this scope

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0425`.

The next lesson puts names into modules and imports them, which is where this error usually comes from in a project bigger than one file.

Exercises

  1. Compile warm.rs with -A unused and then with -D unused_mut. Which messages survive each switch?
  2. Make imports.rs build cleanly without deleting the function, using an attribute on the function. What does the attribute have to say?
  3. Compile mismatch.rs with the annotation changed to u8. What error do you get now, and why is it a different one?

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. Where does rustc write its warnings?

    Answer

    To standard error

    Diagnostics go to standard error, so a program that writes to stdout keeps its output clean.

  2. What does --deny warnings do?

    Answer

    It turns every warning into an error and fails the build

    Deny raises the level of the lint, and a denied lint stops the build like any other error.

  3. What does an attribute such as #[allow(unused_variables)] change?

    Answer

    It silences that lint for the item it is attached to

    allow sets the lint level for that item and everything inside it.

  4. What does a warning mean for the build?

    Answer

    The binary was produced and the program can run

    A warning is information about code that compiles; only a denied lint or an error stops the build.

  5. What does this program print? fn main() { let mut count = 0; let step = 2; count += step; println!("count: {count}"); }

    Answer

    count: 2

    The mutable binding is updated once by the value of step, and mut is justified here.

  6. What is the first line of the compiler output for this program? fn main() { let total: i32 = "12"; println!("{total}"); }

    Answer

    error[E0308]: mismatched types

    The annotation asks for i32 and the literal is a &str, and Rust converts neither direction implicitly.

  7. What is the first line of the compiler output for this program? fn main() { let total = price + 1; println!("{total}"); }

    Answer

    error[E0425]: cannot find value price in this scope

    The name price was never declared, and the caret points at the identifier in the expression.

  8. What does this program print? fn main() { let code = 7; if code > 0 { println!("positive"); } else { println!("not positive"); } }

    Answer

    positive

    Seven is greater than zero, so the first branch prints and the else branch is not run.

  9. What does this program print after the warnings? fn main() { let unused = 5; let mut total = 0; println!("total: {}", total); }

    Answer

    total: 0

    Warnings do not stop the build, and total is still zero when it is printed.

  10. Which lint reports a statement that follows a return?

    Answer

    unreachable_code

    unreachable_code marks code that can never run, as in the warning the lesson shows.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev