Jump to content

3 Rust Debugging Tricks Every Developer Should Know

From JOHNWICK
Revision as of 00:12, 16 November 2025 by PC (talk | contribs) (Created page with "Stop guessing. Fix the cause. Ship confident code. This article gives three surgical, practical debugging tricks for Rust projects. Each trick is short, hands on, and built so that you can use it in the next hour. Code is clear. Benchmarks are realistic examples. Diagrams are hand-drawn style using lines so the architecture remains readable in plain text. Read this as a mentor speaking over coffee. Use these techniques to find the root cause faster, to stop chasing sympt...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Stop guessing. Fix the cause. Ship confident code. This article gives three surgical, practical debugging tricks for Rust projects. Each trick is short, hands on, and built so that you can use it in the next hour. Code is clear. Benchmarks are realistic examples. Diagrams are hand-drawn style using lines so the architecture remains readable in plain text. Read this as a mentor speaking over coffee. Use these techniques to find the root cause faster, to stop chasing symptoms, and to sleep better at release time.


Table of contents

  • Panic hooks and full backtraces: capture the truth
  • Run Miri for undefined behavior in unsafe code
  • Use AddressSanitizer and structured tracing for hard memory bugs
  • Final checklist: what to add to every Rust repo
  • Two image prompts (title image and final-thoughts image) tuned to attract attention


Trick 1 — Panic hooks and full backtraces: capture the truth Problem A crash occurs in production with a short panic message and no useful stack trace. The codebase uses many crates and the next deploy is hours away. The panic message is vague and tests pass locally. Change Install a panic hook that prints a full backtrace and enable RUST_BACKTRACE=1. For deterministic, readable traces, add color-backtrace for better formatting. Why this helps A short panic hides the call chain. The precise route through your code and dependencies that caused the panic is displayed in a complete backtrace. Usually, that is sufficient to identify the offending module. Code Cargo.toml (add once) [dependencies] color-backtrace = "0.6" main.rs fn main() {

   color_backtrace::install(); // install early

// Example: forced panic to demonstrate trace

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

} Minimal note: call color_backtrace::install() as early as possible. This prints symbolicated frames and shows source lines when available. How to run On your dev machine: RUST_BACKTRACE=1 cargo run Example result (example run on a modern laptop) thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 10', src/main.rs:6:20 stack backtrace:

  0: backtrace::backtrace::libunwind::trace
  1: std::panicking::take_hook
  2: color_backtrace::print
  3: core::panicking::panic_fmt
  4: <main as core::ops::function::FnOnce<()>>::call_once
  5: std::rt::lang_start::Template:Closure
  6: std::rt::lang_start_internal

Result summary

  • Problem: crash with little information.
  • Change: installed color-backtrace and enabled full backtraces.
  • Result: immediate, symbolicated stack trace that points to src/main.rs:6:20. Time to root cause: seconds rather than hours.

Hand-drawn style diagram (call flow) [app start]

   |
   v

[color_backtrace::install()]

   |
   v

[main code] -> [panic occurs]

   |
   v

[color-backtrace prints full trace]


Trick 2 — Run Miri to find undefined behavior in unsafe code Problem Intermittent crashes, data corruption, or logic that cannot be explained by safe Rust errors. A few modules use unsafe blocks for performance and low-level interop. Change Run Miri on unit tests or small reproductions. Miri is a Rust interpreter that can identify undefined behavior in unsafe code, including use-after-free, misaligned loads, invalid enum discriminants, and invalid pointer use. Why this helps Rust’s safety guarantees apply only to safe code. Unsafe blocks may introduce subtle UB that appears as random failures. Miri finds these issues quickly without needing complex fuzzers. How to run Install Miri and run for tests: rustup component add miri cargo miri test To run a single test: cargo miri test --test foo Example minimal unsafe code that hides UB // src/lib.rs pub unsafe fn read_unaligned_u32(buf: *const u8) -> u32 {

   // incorrect: this assumes alignment
   std::ptr::read(buf as *const u32)

} Test that triggers UB

  1. [test]

fn test_unaligned() {

   let data = [0u8; 5];
   unsafe {
       let v = crate::read_unaligned_u32(data.as_ptr().add(1));
       let _ = v;
   }

} Miri output (example) error: Undefined Behavior: memory access is not aligned to 4 bytes

 --> src/lib.rs:3:5
  |

3 | std::ptr::read(buf as *const u32)

  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ memory access aligns to 4 but pointer is 1

Result summary

  • Problem: intermittent corruption from unsafe code.
  • Change: run cargo miri test.
  • Result: Miri reports misaligned memory access. Fix by using ptr::read_unaligned or rework data layout. Time to find issue: minutes.

Hand-drawn style diagram (Miri workflow) [Unit tests] -> [cargo miri test]

      |
      v

[Miri interpreter]

      |
      v

[Undefined Behavior errors reported]


Trick 3 — Use AddressSanitizer and structured tracing for hard memory bugs Problem A crash in release mode that does not reproduce in debug builds. The bug looks like memory corruption. Tests pass locally. Change Build with AddressSanitizer to detect heap/stack buffer overflows and use tracing to instrument the suspicious path with structured spans. Combine sanitizer output with trace context to localize the failing code path. Why this helps Sanitizers find many classes of memory errors that are invisible otherwise. Structured tracing makes logs searchable and attaches span context to errors. Set up AddressSanitizer (nightly is required for compiler sanitizer support)

  • Use nightly toolchain.

rustup default nightly

  • Build with sanitizer:

RUSTFLAGS="-Z sanitizer=address" \

   cargo +nightly run
  • Or for tests:

RUSTFLAGS="-Z sanitizer=address" \

   cargo +nightly test

Add tracing Cargo.toml [dependencies] tracing = "0.1" tracing-subscriber = "0.3" instrumented_code.rs use tracing::{info, instrument};

  1. [instrument]

pub fn process(buf: &mut [u8]) {

   info!(len = buf.len(), "start process");
   // hypothetical unsafe call
   unsafe { raw_process(buf) }

} unsafe fn raw_process(_b: &mut [u8]) {

   // placeholder for unsafe logic

} main.rs fn main() {

   tracing_subscriber::fmt::init();
   let mut b = vec![0u8; 16];
   process(&mut b);

} Benchmarks (example microbenchmark for logging cost) Benchmark code (simple loop) use std::time::Instant;

fn no_log() {

   for _ in 0..100_000 { let x = 1 + 2; let _ = x; }

} fn with_logging() {

   for _ in 0..100_000 {
       tracing::info!("tick");
   }

} fn main() {

   tracing_subscriber::fmt::fmt().with_max_level(tracing::Level::INFO).init();
   let t = Instant::now();
   no_log();
   println!("no_log: {:?}", t.elapsed());
   let t2 = Instant::now();
   with_logging();
   println!("with_log: {:?}", t2.elapsed());

} Example run on a development laptop (example numbers): no_log: 8.2 ms with_log: 620 ms Switching to Level::WARN to disable info! logs reduces with_log to: with_log (disabled): 9.0 ms Explanation

  • Problem: tracing at info level is expensive if the recorder formats strings and writes to IO for each event.
  • Change: lower logging level in hot loops or use tracing with Event filtering and lightweight fields.
  • Result: when logging is disabled at runtime by level, cost drops near the no-log baseline. When enabled for debugging, the extra cost is acceptable for short runs and gets excellent contextual information.

Combining with AddressSanitizer

  • Run sanitized binary while instrumenting the failing path with tracing spans. When sanitizer triggers, the logs immediately show the span context and the sanitizer stack trace shows the low-level memory fault. That combination reduces the time from crash to fix.

Result summary

  • Problem: release-only crash and noisy logs.
  • Change: run with AddressSanitizer and add tracing spans. Lower log level in hot code.
  • Result: sanitizer pinpointed overflow; tracing helped map to high-level operation that invoked the unsafe block. Time to fix: same day instead of the next sprint.

Hand-drawn style diagram (sanitizer + tracing) [Start run with ASAN] -> [instrumented app with tracing]

         |
         v

[ASAN detects buffer overflow] -> [sanitizer output + stack]

         |
         v

[tracing logs show high-level span context]


Final checklist: what to add to every Rust repository

  • Add color-backtrace or ensure RUST_BACKTRACE=1 for development runs.
  • Add miri to developer setup instructions and cargo miri test to CI for crates with unsafe code.
  • Document how to build and run with AddressSanitizer using nightly in README.
  • Use tracing for structured logs and add a default RUST_LOG configuration file for developers.
  • Add a test that reproduces the smallest failing case and keep it under tests/ for fast iteration.


Closing mentor note These three techniques are not a silver bullet. They are reliable tools. Use panic hooks to see the full story. Use Miri to eliminate undefined behavior. Use sanitizers and structured tracing to connect low-level faults with high-level intent. Adopt them early. Teach the team. Each saved debugging hour compounds over the life of the project. You will ship with less uncertainty. You will learn how Rust fails and how Rust succeeds.