How to Use the dbg! Macro for Quick Debugging

The `dbg!` macro is a built-in Rust tool that evaluates an expression, prints its value along with the source file and line number to stderr, and then returns the original value so execution continues uninterrupted.

The self-documenting probe

You're tracing a data pipeline. The final result is garbage, but you have no idea where the corruption started. You reach for println!, type the format string, realize you forgot the variable name, fix it, compile, run, and stare at a number with no context. You add another println! for the next step. Now your code is littered with format strings, and you're spending more time managing debug output than hunting the bug.

The dbg! macro solves this. You wrap any expression in dbg!, and Rust prints the source file, line number, the expression itself, and its value to standard error. The macro then returns the value, so your code continues exactly as if you hadn't added the debug call. It's a self-documenting probe. You get context and value in one shot, with zero formatting boilerplate.

How dbg! works

dbg! is a built-in macro that evaluates an expression, prints diagnostic information, and returns the result. It prints to standard error, which keeps debug noise separate from your program's standard output. If you pipe stdout to a file, dbg! output still hits the terminal. This separation is intentional. It prevents debug probes from corrupting data streams or log files.

The macro relies on the Debug trait. Any type you inspect must implement Debug. Standard types like integers, strings, and vectors implement Debug automatically. Custom structs do not. If you try to dbg! a struct without Debug, the compiler rejects the code. You'll see E0277 (trait bound not satisfied) with a suggestion to add #[derive(Debug)]. Add the derive, and dbg! works immediately.

Under the hood, dbg! expands to a block expression. It evaluates the argument once, stores the result in a temporary variable, prints using eprintln!, and returns the temporary. The macro uses stringify! to capture the source text of the expression. This is why dbg!(x + y) prints x + y in the label, not the computed value. The macro captures the syntax, not the runtime result, for the diagnostic label.

Minimal example

fn main() {
    let x = 10;
    
    // dbg! evaluates x + 5, prints the file, line, expression, and value,
    // then returns 15 so the assignment works normally.
    let y = dbg!(x + 5);
    
    // y holds 15. The dbg! call did not change the value.
    println!("y is {}", y);
}

Run this code, and you'll see output like [src/main.rs:5] x + 5 = 15 on stderr, followed by y is 15 on stdout. The dbg! call inserted a diagnostic without breaking the flow. The variable y receives the result of the expression, exactly as if dbg! weren't there.

Macro expansion and mechanics

Understanding the expansion helps you predict behavior. dbg!(expr) expands roughly to this structure:

{
    let val = expr;
    eprintln!("[{}:{} {} = {:?}", file!(), line!(), stringify!(expr), val);
    val
}

The block evaluates expr once. This matters for side effects. If expr calls a function, that function runs. dbg!(compute_heavy()) executes compute_heavy. The macro doesn't peek at the value without running it. It runs the expression, prints the result, and yields the value.

The stringify! macro captures the literal text of expr. If you write dbg!(x + y), the label shows x + y. If you write dbg!(foo()), the label shows foo(). This makes it easy to grep for specific probes in large outputs. The brackets [...] are part of the format. They help distinguish dbg! output from other logs.

Convention aside: dbg! output includes the file path and line number. This is invaluable in large projects. When you see [src/lib.rs:42], you know exactly where the probe lives. You can click the line number in many editors to jump to the source. Treat the output as a breadcrumb trail.

Realistic example

fn calculate_shipping(weight: f64, distance: f64) -> f64 {
    // Base rate depends on weight.
    let base = weight * 2.0;
    
    // dbg! helps verify the multiplier logic without breaking the chain.
    // It prints the expression and value, then returns the multiplier.
    let multiplier = dbg!(1.0 + (distance / 100.0));
    
    // Calculate final cost using the probed multiplier.
    let total = base * multiplier;
    
    // dbg! at the end of a block returns the value,
    // acting as both a probe and the return statement.
    dbg!(total)
}

fn main() {
    let cost = calculate_shipping(5.0, 150.0);
    println!("Cost: {}", cost);
}

This example shows dbg! in a calculation chain. The probe on multiplier lets you verify the distance logic. The probe on total inspects the result before returning. Because dbg! returns the value, you can use it as the final expression in a block. It replaces return total; with dbg!(total), giving you a diagnostic for free.

The Debug trait requirement

dbg! requires Debug. This is a hard requirement. If you have a custom type, you must implement Debug. The easiest way is #[derive(Debug)]. This generates a Debug implementation that prints the struct fields in a readable format.

#[derive(Debug)]
struct Order {
    id: u32,
    total: f64,
}

fn main() {
    let order = Order { id: 1, total: 99.99 };
    
    // dbg! works because Order derives Debug.
    dbg!(order);
}

Without the derive, the compiler emits E0277. The error message is explicit. It tells you Debug is not implemented and suggests the derive. This is a common friction point for beginners. The fix is always the same: add the derive. If you can't add the derive because the type is from an external crate, you can't use dbg! on it. You'll need println!("{:?}", ...) with a custom formatter or a different debugging strategy.

Convention aside: Debug output can be verbose. Large collections print all their elements. If you dbg! a vector of a thousand items, your terminal floods. Use dbg! on slices or small subsets if the collection is huge. dbg!(&vec[..10]) peeks at the head. dbg!(vec.len()) checks the size without dumping the contents.

Release mode and side effects

dbg! is conditional. It only prints when debug_assertions are enabled. In a release build, dbg! compiles to a no-op for printing. It evaluates the expression but prints nothing. This prevents accidental debug leaks in production.

This behavior has a trap. dbg! evaluates the expression in release mode even if it doesn't print. dbg!(compute_heavy()) runs compute_heavy in release. If you want to skip the computation, you need #[cfg(debug_assertions)] around the call.

fn main() {
    #[cfg(debug_assertions)]
    let result = dbg!(compute_heavy());
    
    #[cfg(not(debug_assertions))]
    let result = compute_heavy();
    
    println!("Result: {}", result);
}

This pattern ensures compute_heavy only runs in debug builds. It's verbose, but it avoids performance regressions. If the expression is cheap, you can leave dbg! in release builds. The overhead is just the evaluation. If the expression is expensive, guard it with cfg.

Pitfall: dbg! has side effects. If the expression mutates state, the state changes. dbg!(counter.fetch_add(1)) increments the counter. This is usually what you want, but be aware. dbg! is not a peek. It's an evaluation. If you need to inspect a value without running code, you can't use dbg!. You need a debugger or a different approach.

Pitfalls and conventions

dbg! is powerful, but it has limits. It prints to stderr. If your program captures stderr, dbg! output goes into the capture. This can be useful or annoying depending on your setup. If you're writing a library, dbg! output might leak to the user's terminal. Libraries should avoid dbg!. Use log or tracing for library diagnostics. dbg! is for application code and quick experiments.

Performance matters. dbg! does I/O. I/O is slow. Don't put dbg! in a tight loop over a million items. You'll hang the program. Use sampling or a debugger for loops. dbg! is for spot checks, not continuous monitoring.

Convention aside: dbg! with no arguments prints the file and line. dbg!() is useful for "I reached here" probes. It confirms execution flow without inspecting a value. Use it at the start of branches or functions to verify control flow.

Another convention: remove dbg! when the bug is dead. dbg! is temporary. It clutters code and adds overhead. Use git diff or your editor's search to find and remove probes after debugging. Some teams use a pre-commit hook to block dbg! in production code. This enforces the cleanup discipline.

Section closer: Treat dbg! as a probe, not a log. Remove it when the bug is dead.

Decision: when to use dbg!

Use dbg! when you need a quick, one-shot inspection of an expression with automatic context. Use dbg! when you want to inspect values in a chain of operations without breaking the flow. Use println! when you need custom formatting or want output that persists in release builds. Use a debugger like lldb or gdb when you need to step through code, inspect memory, or set conditional breakpoints. Use log or tracing when you need structured logging that can be filtered by level and target in production. Use assert! when you need to verify a condition and panic if it's false. dbg! observes; it does not validate.

Where to go next