Skip to content
AgileLogics
6 min read

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: and note: 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 for dbg! 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]));
}