How to Use the Debug Trait for Debugging Output

Enable debug output for custom structs by adding #[derive(Debug)] and using the {:?} format specifier in println!.

When the compiler refuses to print your struct

You just wrote a struct to hold configuration data. You create an instance, pass it through a function, and want to inspect the values. You type println!("{}", config). The compiler rejects you with E0277 (trait bound not satisfied). It tells you the struct doesn't implement std::fmt::Display. You haven't told Rust how to turn your data into text, and Rust refuses to guess.

This is the moment you meet the Debug trait. It is the standard contract for developer-facing output. When you need to see the internal state of a value, Debug is the mechanism. It handles the formatting so you can focus on the logic.

What Debug actually does

Rust separates debugging output from user-facing output. The Debug trait defines how a type should be represented for a developer looking at logs, a debugger, or a console dump. It is not for pretty text in a UI. It is for raw, structural inspection.

Think of Debug like the raw JSON dump of an object in JavaScript, or the __repr__ method in Python. It shows the type name, the field names, and the field values. It gives you the skeleton of the data. When you derive Debug, the compiler generates the implementation automatically. It writes the code that iterates over your fields and formats them. You get a consistent, readable representation without writing a single line of formatting logic.

Minimal example

The standard workflow is to add the #[derive(Debug)] attribute to your type and use the {:?} format specifier.

/// A simple geometric point.
#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 10, y: 20 };
    
    // The {:?} specifier invokes the Debug trait.
    // Without #[derive(Debug)], this line fails to compile.
    println!("Point is {p:?}");
}

The output is Point is Point { x: 10, y: 20 }. The {:?} tells the formatter to look for the Debug implementation. The derive macro generates that implementation behind the scenes.

Convention aside: Use the new inline syntax {p:?} instead of the older {p:?} positional syntax. It reads better and reduces boilerplate. The community treats the inline form as the default for modern Rust code.

How the derive macro works

When you write #[derive(Debug)], the compiler expands this into an impl block. It generates a fmt method that takes a &mut Formatter and writes the representation. The generated code respects the structure of your type. For structs, it writes the name, an opening brace, comma-separated fields, and a closing brace. For enums, it handles the variants and their associated data.

If you forget the derive attribute, the compiler has no Debug impl to call. You get E0277 pointing to the missing trait. The error message usually suggests adding #[derive(Debug)] or implementing the trait manually. Trust the suggestion. Derive is almost always what you want.

Realistic usage with nested types

Real code involves enums, nested structs, and collections. Debug handles these recursively. If your struct contains other types that implement Debug, the output nests automatically.

/// Represents the state of a game session.
#[derive(Debug)]
enum GameState {
    Menu,
    Playing { score: u32, level: u8 },
    GameOver { reason: String },
}

/// A player with associated state.
#[derive(Debug)]
struct Player {
    name: String,
    state: GameState,
}

fn main() {
    let player = Player {
        name: "Alice".to_string(),
        state: GameState::Playing { score: 1500, level: 3 },
    };
    
    // Debug formats nested structures automatically.
    // The inner GameState is printed as part of the Player output.
    println!("Current player: {player:?}");
}

The output shows the full hierarchy. You can see Player contains state, and state contains the Playing variant with its fields. This recursive behavior makes Debug incredibly useful for complex data structures. You don't need to write custom formatters for every layer.

Convention aside: Use {:#?} for pretty-printing. The # flag adds indentation and newlines. When dumping large structures, println!("{:#?}", data) is the standard practice. It makes nested data readable. The # modifier works with any Debug implementation that respects flags, including derived ones.

The dbg! macro

For quick debugging, dbg!() is the workhorse. It prints the value, the file name, and the line number to stderr, then returns the value unchanged. This lets you chain it into expressions.

fn calculate_total(items: &[u32]) -> u32 {
    // dbg! prints the value and location, then returns the sum.
    // This allows you to inspect intermediate values without breaking the chain.
    let sum: u32 = items.iter().sum();
    dbg!(sum);
    
    sum * 2
}

fn main() {
    let total = calculate_total(&[10, 20, 30]);
    println!("Final total: {total}");
}

The output goes to stderr, so it doesn't mix with stdout if you are piping program output. The macro returns the value, so you can write let x = dbg!(compute_expensive_value()) to log the result while keeping the variable. This is faster than writing println! and assigning separately.

Convention aside: Remove dbg! calls before production. They clutter logs and can leak sensitive data. Use them during development, then strip them out. Some teams use #[cfg(debug_assertions)] to gate debug code, but dbg! is usually just deleted.

Manual implementation

Sometimes derive isn't enough. You might have sensitive data that shouldn't be printed, or you might want a custom representation that still serves debugging purposes. In these cases, implement Debug manually.

use std::fmt;

/// A secret key that should not be logged in plain text.
struct SecretKey {
    key: String,
}

// Manual impl to redact the key in debug output.
impl fmt::Debug for SecretKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Write a redacted version instead of the raw string.
        // This prevents accidental leakage in logs.
        write!(f, "SecretKey {{ key: [REDACTED] }}")
    }
}

fn main() {
    let key = SecretKey { key: "super-secret-123".to_string() };
    println!("{key:?}");
}

The output is SecretKey { key: [REDACTED] }. You control exactly what appears. This is essential for types holding passwords, tokens, or PII.

Convention aside: Use the builder API for manual implementations. Instead of write!, use f.debug_struct("Name"). The builder API respects formatting flags like # automatically. If you use write!, you have to handle flags manually. The builder API is the community standard for manual Debug impls.

use std::fmt;

struct Complex {
    re: f64,
    im: f64,
}

impl fmt::Debug for Complex {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // The builder API handles flags and formatting consistently.
        // It produces output that looks identical to #[derive(Debug)].
        f.debug_struct("Complex")
            .field("re", &self.re)
            .field("im", &self.im)
            .finish()
    }
}

Use f.debug_struct, f.debug_tuple, or f.debug_enum depending on your type. Call .field() for each field. End with .finish(). This approach is robust and maintainable.

Pitfalls and errors

Recursive types break derive. If a struct contains itself directly, the size is infinite and the compiler rejects it.

// This fails to compile.
// #[derive(Debug)]
// struct Node {
//     value: i32,
//     next: Node, // Infinite recursion.
// }

You get an error about infinite size or recursion. The fix is to use indirection. Wrap the recursive field in Box or Rc.

#[derive(Debug)]
struct Node {
    value: i32,
    // Box breaks the recursion. The size is finite.
    next: Box<Node>,
}

Private fields in other crates are invisible. If you derive Debug on a struct in your crate, and a field is a type from another crate that doesn't implement Debug, you get an error. You can't derive Debug if any field lacks it. This enforces a chain of visibility. If you need to debug a type with opaque fields, implement Debug manually and skip the opaque parts.

Using {} instead of {:?} is a common mistake. {} requires Display. If you only have Debug, the compiler rejects you with E0277. The error message distinguishes between Display and Debug. Check the specifier. If you want user-facing text, implement Display. If you want debugging text, use {:?}.

Decision: when to use Debug vs alternatives

Use #[derive(Debug)] when you need a quick, structural dump of a type for logging or debugging.

Use {:?} when you are printing to the console or writing to a log file for developer consumption.

Use {:#?} when the output is nested and you need indentation to read it.

Use a manual impl Debug when the default output leaks sensitive data or when you want a custom representation that still serves debugging purposes.

Use the builder API (f.debug_struct) for manual implementations to ensure consistent formatting and flag support.

Use dbg!() when you need a quick macro that prints the expression value, file, and line number without typing println!.

Use Display (the {} specifier) when you are formatting text for end-users, not for debugging.

Reach for std::fmt::Debug documentation when you need to understand the Formatter API or flag handling.

Derive is the default. Manual impl is the override. If you can derive it, do. If you can't, build it.

Where to go next