Debugging Rust Code: A Complete Guide to Techniques and Tools

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Debugging Rust Code: From "println!" to Pro Tools



Anyone who has written Rust for at least a week knows this feeling: the compiler passed, the program compiled, it starts... and does something completely unexpected. Or it just crashes with a mysterious Segmentation fault. Welcome to the world of debugging! The good news: Rust has a whole arsenal for this — from a simple macro to advanced debuggers. In this article, we'll cover all ways of debugging Rust code, from basic to professional.



1. The Gentleman's Set: dbg! and eprintln! Macros



Let's start with the simplest and most accessible. If you're writing Rust code and want to quickly understand what's happening in variables — use dbg!. It prints both the variable name, its value, and the file with the line number. Much more convenient than the good old println!.



fn calculate_even_sum(numbers: &[i32]) -> i32 {    let mut sum = 0;    for &n in numbers {        if n % 2 == 0 {            sum += n;            dbg!(n, sum);  // look at each even number and the current sum        }    }    sum}

fn main() { let data = vec![1, 2, 3, 4, 5, 6]; let result = calculate_even_sum(&data); dbg!(result);}


Run it — and you'll see something like:



[src/main.rs:6:13] n = 2[src/main.rs:6:13] sum = 2[src/main.rs:6:13] n = 4[src/main.rs:6:13] sum = 6[src/main.rs:6:13] n = 6[src/main.rs:6:13] sum = 12[src/main.rs:12:5] result = 12


And if you need to write to stderr (for example, to avoid interfering with stdout during pipes) — use eprintln!. This is indispensable for debugging Rust code in a production environment.



2. Advanced Logging with log and env_logger



When a project grows, scattering dbg! throughout the code becomes painful. The log and env_logger crates come to the rescue. They allow you to enable/disable logs without recompilation.



use log::{info, warn, error, debug};

fn main() { env_logger::init();

info!("Program started"); let x = 42; debug!("Value of x: {}", x);

if x > 100 { warn!("x is too large!"); } else { error!("This is not an error, just a check"); }}


Run it with the environment variable RUST_LOG=debug cargo run — and you'll get all the logs. Only want warnings? RUST_LOG=warn. Very convenient for debugging Rust applications in real time.



3. Panic and Backtrace: Reading the Error Stack



When a program panics, Rust prints a message. But often that's not enough. Enable a full backtrace:



$ RUST_BACKTRACE=1 cargo run


Or for maximum detail:



$ RUST_BACKTRACE=full cargo run


You'll see the entire call stack — from the crash point to main. This is critically important for debugging Rust code if you don't understand where the error came from.



Example: suppose you have this code:



fn main() {    let v = vec![1, 2, 3];    println!("{}", v[10]);  // panic!}


With RUST_BACKTRACE=1 you'll see not just "index out of bounds", but the exact line and call.



4. Debug and Optimization Modes: debug vs release



An important point: debugging Rust code in debug mode (default) provides full information. But in release mode, the compiler optimizes the code, and some variables may disappear or change order. If you're debugging a "release" bug — use a profile with debug information:



[profile.release]debug = true


In Cargo.toml. This will allow you to run cargo run --release

Blogs

Book Recommendations