What Is the dbg! Macro and How to Use It for Debugging?

The dbg! macro prints an expression's value and location to stderr, then returns the value unchanged for inline debugging.

The breakpoint that prints itself

You are tracing a data transformation pipeline. A value enters a function as 42, passes through three mapping steps, and emerges as 0. You do not want to attach a full debugger. You do not want to rewrite your chain with println! statements that break the flow. You just want to see what is happening at each step without touching the logic.

Rust gives you dbg!. It is a built-in macro that inspects an expression, prints its source location and value to standard error, and returns the value unchanged. You wrap any expression in dbg!() and the compiler injects the inspection code for you. The pipeline keeps running. The output appears in your terminal. You get immediate visibility without refactoring.

This macro exists because Rust developers value rapid feedback loops. Writing a temporary variable, printing it, and deleting it later is tedious. dbg! removes the friction. You inspect, you verify, you move on. The macro is part of the standard library, requires no dependencies, and works in every Rust edition.

Treat dbg! as a temporary instrument. It is excellent for rapid iteration, but it is not a replacement for structured logging or a proper debugger. Remove it before shipping.

How the macro actually works

Macros in Rust are code generators. They run at compile time and replace themselves with expanded Rust code before the compiler runs the type checker. dbg! is no different. When you write dbg!(my_value), the macro expands into a block that captures the current file name, line number, and column. It formats the value using the Debug trait. It writes everything to stderr. Finally, it evaluates and returns the original expression.

Think of it like a transparent checkpoint on a factory conveyor belt. The item passes through without stopping. A camera snaps a photo, logs the timestamp and station number, and keeps the belt moving. The downstream machinery receives the exact same item. The only difference is that you now have a record of what passed through.

The macro relies entirely on the Debug trait. If a type does not implement Debug, the expansion fails. This is a deliberate design choice. Rust forces you to opt into debug formatting rather than guessing how to represent complex data. You add #[derive(Debug)] to your structs and enums to unlock this behavior. The Debug trait produces output meant for developers, not end users. It shows field names, enum variants, and internal state. This verbosity is intentional. You want to see exactly what the compiler sees.

Community convention treats dbg! as a diagnostic tool, not a logging mechanism. When you leave dbg! in a codebase for team review, it is immediately obvious that the line is temporary. println! blends in with normal output and often gets forgotten. If you must leave debug prints for a hotfix, wrap the call in #[cfg(debug_assertions)] so it compiles away in release builds.

Keep the expression inside dbg! free of heavy side effects. The macro is for inspection, not for triggering actions.

A minimal example

Start with a straightforward variable inspection. The macro works on any expression, including literals, variables, function calls, and method chains.

/// Demonstrates basic dbg! usage with a vector and a chained operation.
fn main() {
    let numbers = vec![10, 20, 30];
    
    // dbg! prints the value and returns it, so the assignment still works.
    let doubled = dbg!(numbers.iter().map(|n| n * 2).collect::<Vec<i32>>());
    
    // The returned value is fully usable in the next line.
    let sum: i32 = doubled.iter().sum();
    println!("Final sum: {}", sum);
}

Run this code and watch the terminal. You will see output prefixed with the file path, line number, and a blue arrow pointing to the value. The println! at the end still executes normally. The macro never swallows the result.

The type annotation ::<Vec<i32>> is necessary here because the compiler cannot infer the collection type from the dbg! wrapper alone. The macro returns the exact type of the expression inside it, so type inference flows through unchanged. You do not need to change your variable declarations to accommodate the macro.

Trust the type system. If dbg! compiles, the value will pass through with its original type and lifetime intact.

Walking through the output

When the program runs, dbg! writes to standard error, not standard output. This separation matters when you pipe your program into other tools. stdout carries your application data. stderr carries diagnostics. Most terminals merge them visually, but scripts that capture output will only see stdout unless you redirect 2>&1. This design keeps your program's data stream clean while still showing you what is happening behind the scenes.

The macro expands to something conceptually similar to this:

{
    // Capture location at compile time using built-in macros.
    let _file = file!();
    let _line = line!();
    
    // Print to stderr using Debug formatting.
    eprintln!("[{}:{}]: value = {:?}", _file, _line, expr);
    
    // Return the original expression unchanged.
    expr
}

The actual expansion is slightly more optimized, but the behavior is identical. The macro evaluates the expression once. It formats it. It prints it. It hands it back. This single-evaluation guarantee prevents accidental side effects from running twice. If your expression calls a function that modifies global state, dbg! will not call it twice. The compiler treats the wrapped expression as a single unit.

This behavior also means dbg! works seamlessly with async code. You can wrap an async block or an awaited future, and the macro will print the resolved value after the future completes. The macro does not block the executor. It simply inspects the result once the runtime hands it back.

Do not use dbg! to replace proper error handling. It is a magnifying glass, not a bandage.

Real-world debugging patterns

In practice, you will use dbg! inside complex chains where inserting a temporary variable feels clunky. It shines in iterator pipelines, match arms, and function arguments.

/// Processes user input and filters invalid entries.
fn process_input(raw: &[&str]) -> Vec<String> {
    // Inspect the raw slice before transformation.
    let _ = dbg!(raw);
    
    // Chain dbg! directly inside the iterator without breaking the flow.
    raw.iter()
        .map(|s| s.trim())
        .filter(|s| !s.is_empty())
        .map(|s| {
            // Check the transformed string before pushing to the result.
            dbg!(s.to_uppercase())
        })
        .collect()
}

Notice how dbg! sits inside the map closure. It prints the uppercase string, then returns it so collect() can gather it into the final vector. The pipeline remains a single expression. You avoid introducing intermediate variables that clutter the scope.

When debugging nested data structures, dbg! respects the Debug formatting rules. Structs print with field names. Enums print with variant names. Tuples print with indices. This consistency makes it easy to spot which part of a complex payload is misbehaving. If you need to suppress noisy fields, implement Debug manually and use #[serde(skip_debug)] or custom formatting logic. The macro will respect your custom implementation.

Convention aside: many developers alias dbg! to d! in their personal crates to reduce typing. The standard library does not provide this alias, but a one-line macro definition in lib.rs solves it. Keep the alias consistent across your project to avoid confusion during code review.

Remove diagnostic wrappers before merging to main. They belong in feature branches, not in release tags.

Pitfalls and compiler friction

The most common wall you will hit is the missing Debug trait. If you try to inspect a custom type without deriving Debug, the compiler stops you with E0277 (the trait bound Debug is not satisfied). The error message points directly to the dbg! call and tells you exactly which type is missing the implementation. Add #[derive(Debug)] to the struct or enum definition. The macro will work immediately.

Another friction point appears with Result and Option types. Wrapping a Result<T, E> in dbg! prints the entire wrapper, including the error variant if it is an Err. This often floods your terminal with stack traces or large error payloads when you only wanted to see the success value. Unwrap the value first, or use pattern matching to isolate the part you need.

let maybe_data = fetch_data();

// Prints the full Option, which can be noisy.
// dbg!(maybe_data);

// Prints only the inner value if it exists.
if let Some(data) = maybe_data {
    dbg!(data);
}

Performance is the third consideration. dbg! formats output and writes to stderr. This is not a zero-cost operation. Leaving it inside a tight loop that runs millions of times will throttle your program to a crawl. The I/O bottleneck will dominate your runtime. Profile your code before and after adding debug prints. If the loop slows down, remove the macro or throttle it with a counter.

Trust the borrow checker when dbg! complains about lifetimes. The macro does not extend the lifetime of temporary values. If you try to debug a reference that dies at the end of the statement, you will hit E0716 (temporary value dropped while borrowed). Move the value to a named variable first, then inspect it. The macro cannot magically keep a temporary alive beyond its natural scope.

One more subtle trap involves Display versus Debug. If you are used to println!("{}", value), you expect Display formatting. dbg! always uses {:?} formatting. Types that implement Display but not Debug will fail to compile with dbg!. This is by design. Debug output prioritizes completeness over readability. You want to see the raw structure, not a polished user-facing string.

Keep your diagnostic calls surgical. Inspect the exact boundary where behavior changes, not the entire function.

When to use dbg! versus alternatives

Use dbg! when you need rapid, inline inspection of a value without breaking an expression chain. Use println! or eprintln! when you need custom formatting, conditional output, or want to keep diagnostic code separate from business logic. Use a native debugger like gdb or lldb when you need to step through execution, inspect memory layouts, or catch panics at the exact instruction they occur. Use a structured logging crate like tracing or log when you are building production software that requires log levels, filtering, and remote aggregation.

Where to go next