Debugging Basics
Prerequisites: Comments, rustfmt & Clippy
Rust catches most bugs at compile time, so a large part of debugging Rust is reading compiler errors. The rest is finding logic errors and understanding panics at runtime.
Reading compiler errors
A Rust error message has a consistent structure:
error[E0382]: borrow of moved value: `a`
--> src/main.rs:4:16
|
2 | let a = String::from("hello");
| - move occurs because `a` has type `String`, which does not implement the `Copy` trait
3 | let b = a;
| - value moved here
4 | println!("{a} {b}");
| ^ value borrowed here after move
- The first line gives the error code (
E0382) and a summary. -->points to the file, line, and column.- The code excerpt marks the problem with
^and related places with-and labels. help:andnote:lines often contain the exact fix.
Fix errors from the top down — one early mistake can cause several later errors. For any error code, get a full explanation with examples:
rustc --explain E0382
Syntax
let value = dbg!(expression); // prints file, line, expression, and value
The dbg! macro prints the file and line, the expression's source
code, and its value to standard error — then returns the value, so you
can wrap it around any expression without changing your code's
behavior. Remove dbg! calls once the bug is fixed.
Example
fn main() {
let width = 4;
let height = dbg!(width * 2);
let area = dbg!(width * height);
println!("area = {area}");
}
Expected output
[src/main.rs:3:18] width * 2 = 8
[src/main.rs:4:16] width * height = 32
area = 32
The first two lines come from dbg! on standard error.
Panics
A panic stops the program when something goes wrong that it can't
recover from, such as indexing past the end of a vector or calling
.expect() on a failed parse:
fn main() {
let numbers = vec![1, 2, 3];
let index = numbers.len() + 7;
println!("{}", numbers[index]);
}
thread 'main' panicked at src/main.rs:4:27:
index out of bounds: the len is 3 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
(Newer Rust versions also print the thread's numeric ID after
'main'.) The message tells you where (src/main.rs:4:27) and
what. To see
the chain of function calls that led there, run with a backtrace:
RUST_BACKTRACE=1 cargo run
(In PowerShell: $env:RUST_BACKTRACE=1; cargo run.) Look for the
first lines that mention your own code.
Common mistakes
- Fixing errors from the bottom up. Start with the first error; later ones are often side effects.
- Skipping the
help:lines. They frequently contain the exact change to make. - Leaving
dbg!in finished code. It keeps printing to standard error in release builds too. Search fordbg!before you commit. - Silencing a borrow checker error with
.clone()without understanding it. First work out who owns the value and why the borrow conflicts.
Practice
This function should return the average of a slice of numbers, but
for [1, 2, 4] it prints Average: 2 instead of the expected
2.3333333333333335. Use dbg! to find the problem, then fix it.
fn average(values: &[i32]) -> f64 {
let mut total = 0;
for v in values {
total += v;
}
(total / values.len() as i32) as f64
}
fn main() {
println!("Average: {}", average(&[1, 2, 4]));
}
Show solution
fn average(values: &[i32]) -> f64 {
let mut total = 0;
for v in values {
total += v;
}
// dbg!(total / values.len() as i32) showed 2: integer division
// throws away the fraction. Convert to f64 *before* dividing.
total as f64 / values.len() as f64
}
fn main() {
println!("Average: {}", average(&[1, 2, 4]));
}