Debugging Rust Programs
Debugging Rust applications requires understanding both compile-time errors and runtime issues. Rust's strong type system catches many bugs early, but when problems occur, you'll need tools like debug prints, logging, and debuggers to diagnose issues efficiently.
1. Solving Compiler Errors
Rust's compiler provides exceptionally helpful error messages. Learning to read them is your first debugging tool.
// Example error scenario
fn main() {
let s = String::from("hello");
print_string(s);
println!("{}", s); // ERROR: value borrowed after move
}
fn print_string(s: String) {
println!("{}", s);
}
Error Message Breakdown:
- Clear explanation: "value used here after move"
- Shows exact line numbers where move occurs
- Suggests fixes: "consider cloning the value"
Tips:
- Read messages from top to bottom
- Look for
help:annotations - Use
cargo checkfor fast feedback
2. Print Debugging Techniques
Quick-and-dirty debugging with print statements remains useful in Rust.
// 1. Basic debug printing
println!("{:?}", some_value); // Requires Debug trait
// 2. Pretty-printing
println!("{:#?}", complex_struct);
// 3. Debug macros
dbg!(&some_var); // Prints file/line and value
// 4. Conditional debugging
#[cfg(debug_assertions)]
println!("[DEBUG] State: {:?}", state);
Best Practices:
- Use
dbg!()for temporary debugging (removed later) - Implement
Debugtrait for custom types - Add
#[derive(Debug)]to structs/enums
3. Structured Logging
Production-grade debugging with log levels and structured data.
use log::{info, error, warn, debug};
fn process_data(data: &Data) -> Result<(), Error> {
debug!("Processing {:?}", data);
if data.is_invalid() {
warn!("Invalid data detected");
return Err(Error::InvalidData);
}
info!("Processed {} records", data.len());
Ok(())
}
// Initialize logger (in main)
env_logger::Builder::from_env(
env_logger::Env::default().default_filter_or("info")
).init();
Logging Levels:
error!- Critical failureswarn!- Potential issuesinfo!- General operationdebug!- Diagnostic infotrace!- Verbose tracing
4. Using Debuggers
Step-through debugging with VS Code or command-line tools.
VS Code Setup:
- Install
CodeLLDBextension - Create
.vscode/launch.json:{ "version": "0.2.0", "configurations": [ { "type": "lldb", "request": "launch", "name": "Debug", "program": "${workspaceFolder}/target/debug/my_app", "args": [], "cwd": "${workspaceFolder}" } ] }
Command Line:
# Build with debug symbols
cargo build
# Launch in GDB
gdb target/debug/my_app
# Basic GDB commands:
# break main.rs:10 # Set breakpoint
# run # Start execution
# next # Step over
# step # Step into
# print x # Inspect variable
5. Runtime Diagnostics
Advanced debugging tools for complex issues.
// 1. Backtraces on panic
RUST_BACKTRACE=1 cargo run
// 2. Memory debugging
#[cfg(debug_assertions)]
{
use std::alloc::{GlobalAlloc, System};
struct DebugAlloc;
unsafe impl GlobalAlloc for DebugAlloc {
unsafe fn alloc(&self, layout: std::alloc::Layout) -> *mut u8 {
println!("Allocating {} bytes", layout.size());
System.alloc(layout)
}
}
#[global_allocator]
static ALLOCATOR: DebugAlloc = DebugAlloc;
}
// 3. Thread inspection
std::thread::current().id() // Get thread ID
std::thread::panicking() // Check if panicking
Diagnostic Crates:
backtrace- Custom backtrace capturetracing- Advanced instrumentationvalgrind- Memory/thread analysis (Linux)
6. Debug Build Configuration
Optimized debugging with Cargo profiles.
# Cargo.toml
[profile.dev]
opt-level = 0 # No optimization (better debug)
debug = true # Include debug symbols
debug-assertions = true # Runtime checks
[profile.dev-override]
opt-level = 1 # Slightly optimized dependencies
# Environment variables
RUSTFLAGS="-C target-cpu=native" # CPU-specific debugging
Debug vs Release:
cargo build- Debug build (default)cargo build --release- Optimized builddebug-assertions- Extra runtime checks in debug