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 = 12And 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 runOr for maximum detail:
$ RUST_BACKTRACE=full cargo runYou'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 = trueIn Cargo.toml. This will allow you to run cargo run --release