How to Make a Struct Printable with Debug and Display

Derive Debug for quick debugging output and implement Display for custom user-friendly formatting.

The compiler refuses to print your struct

You are building a command-line tool. You have a Config struct loaded from a file. Something is going wrong in the parsing logic, so you slap a println!("{}", config) into your code to see what the data looks like. You hit run. The compiler screams.

error[E0277]: the trait `Display` is not implemented for `Config`

Rust will not print a value unless you explicitly tell it how. This is not a limitation. It is a design choice that forces you to make a deliberate decision about presentation. Every type in Rust can choose how it appears to the developer during debugging, and how it appears to the user in production output. These are two different jobs, handled by two different traits.

Debug for you, Display for the world

Rust splits printing into two responsibilities. Debug is for developers. It shows the internal state of a value. It is the raw data dump. Display is for users. It shows a polished, human-readable representation. It is the public face of the value.

Think of a car. Debug is the mechanic's manual with torque specs, part numbers, and wiring diagrams. It is dense, technical, and useful when you are fixing the engine. Display is the brochure with the photo of the car on a mountain road. It highlights the features that matter to the buyer and hides the complexity.

You derive Debug almost automatically. You implement Display only when you have a specific story to tell.

The minimal setup

Start with #[derive(Debug)]. This macro generates a default implementation that prints the struct name and all its fields. It is recursive. If your struct contains other structs, they also need Debug.

#[derive(Debug)] // Generates a default Debug implementation automatically.
struct Rectangle {
    width: u32,
    height: u32,
}

// Display requires manual implementation because there is no single "right" way to show data to a user.
impl std::fmt::Display for Rectangle {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // write! returns a Result to handle potential I/O errors.
        // println! swallows these errors, but the trait requires you to propagate them.
        write!(f, "Rectangle({}, {})", self.width, self.height)
    }
}

fn main() {
    let rect = Rectangle { width: 30, height: 50 };

    // {:?} invokes Debug.
    println!("Debug: {:?}", rect);

    // {} invokes Display.
    println!("Display: {}", rect);
}

Convention aside: The community treats Debug output as unstable for parsing. Never write code that parses the string produced by {:?}. The output format can change between Rust versions or when fields are added. If you need machine-readable serialization, use a crate like serde to produce JSON or TOML. Debug is for eyes, not machines.

Derive Debug first. It is the path of least resistance and saves hours of logging time.

How the formatting machinery works

The println! macro is a facade. It expands into calls on a Formatter object. When you write {:?}, the macro checks if the type implements Debug and calls Debug::fmt. When you write {}, it checks for Display and calls Display::fmt.

The compiler enforces these checks at compile time. If you use {} on a type that only has Debug, you get E0277 (the trait Display is not implemented for Rectangle). The error message usually suggests adding #[derive(Debug)] or implementing the trait, but it will not automatically switch you to {:?}. You have to choose the right format specifier.

The fmt method signature looks intimidating but is consistent across all formatting traits:

fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result

The &self is the value being formatted. The f is the formatter, which carries context like alignment, width, and precision flags. The return type is std::fmt::Result, which is an alias for Result<(), std::fmt::Error>. You return Ok(()) on success or propagate errors. The write! macro handles this boilerplate for you.

Trust the borrow checker here. The &mut Formatter allows the formatting logic to write into the output buffer efficiently without allocating intermediate strings.

Real-world formatting with flags

Real applications often need formatted output that respects user preferences. A table printer might ask for columns to be right-aligned to a width of 10 characters. A currency formatter might ask for two decimal places. These requests come through as flags in the format string, like {:>10} or {:.2}.

If you implement Display naively, you might ignore these flags. This breaks composition. If a user writes println!("{:>10}", my_value), they expect alignment. If your Display impl ignores it, the output looks broken.

You must forward the flags from the outer formatter to your inner writes. The Formatter provides methods like width(), align(), and precision() to query these flags.

use std::fmt;

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Query the formatter for requested width and alignment.
        // unwrap_or provides a default if the caller didn't specify a flag.
        let width = f.width().unwrap_or(0);
        let align = f.align().unwrap_or(fmt::Alignment::Left);

        // Build the content string.
        let content = format!("({}, {})", self.x, self.y);

        // Use the formatter's methods to apply padding correctly.
        // This ensures {:>10} or {:<10} works as expected.
        if width > 0 && content.len() < width {
            let padding = width - content.len();
            match align {
                fmt::Alignment::Left => write!(f, "{}{:padding$}", content, ""),
                fmt::Alignment::Right => write!(f, "{:padding$}{}", "", content),
                fmt::Alignment::Center => {
                    let left = padding / 2;
                    let right = padding - left;
                    write!(f, "{:left$}{}{:right$}", "", content, "")
                }
            }
        } else {
            write!(f, "{}", content)
        }
    }
}

fn main() {
    let p = Point { x: 1, y: 2 };
    // The alignment flags are respected by our implementation.
    println!("[{:>15}]", p);
    println!("[{:<15}]", p);
}

Convention aside: For simple types, you can often avoid manual flag handling by using write! with the flags directly. If you are wrapping a single value, write!(f, "{:>width$}", self.value, width = f.width().unwrap_or(0)) forwards the width automatically. Manual padding is only necessary when you are constructing complex strings where flags apply to the whole result, not just a sub-part.

Respect the flags. The caller asked for alignment; give it to them.

Pitfalls and compiler traps

Nested structs without Debug

The #[derive(Debug)] macro is recursive. If your struct contains a field that does not implement Debug, the derive fails. This is a common stumbling block when you add a new field and forget to derive Debug on the nested type.

#[derive(Debug)]
struct Address {
    city: String,
}

struct User {
    name: String,
    address: Address,
}

fn main() {
    let user = User {
        name: "Alice".to_string(),
        address: Address { city: "Seattle".to_string() },
    };
    // Error: the trait `Debug` is not implemented for `User`
    // because `Address` lacks Debug.
    println!("{:?}", user);
}

The compiler error points to User, but the root cause is Address. Add #[derive(Debug)] to Address and the problem vanishes.

Display returning errors

The fmt method must return std::fmt::Result. If you use f.write_str() or write!, you get a Result. You cannot return (). If you forget the return, you get E0308 (mismatched types).

impl std::fmt::Display for Rectangle {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // Error: expected `Result<(), Error>`, found `()`
        f.write_str("Rectangle")
    }
}

Fix this by returning the result of the write call. write! is a macro that returns the result, so write!(f, "...") is both the action and the return value.

Infinite recursion

If your Display implementation calls println! or format! on self, you create infinite recursion. The formatter calls fmt, which calls format!("{}", self), which calls fmt again. The stack overflows.

// DANGER: Stack overflow
impl std::fmt::Display for Rectangle {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // This calls Display::fmt on self again.
        write!(f, "{}", self)
    }
}

Never format self inside its own fmt implementation. Format the fields, not the struct itself.

Display is a contract with the user. Keep it clean, keep it finite, and never recurse.

When to use what

Use #[derive(Debug)] when you need quick logging and debugging output for development. It provides a complete dump of internal state with zero effort.

Use impl Display when you need a user-facing string representation. This includes CLI output, UI labels, and any text shown to humans who do not care about field names or memory addresses.

Use manual impl Debug when the default output is misleading or too verbose. This is rare. You might hide sensitive fields, truncate large buffers, or group related fields for readability.

Use format! when you need a String value instead of printing to stdout. format! uses the same trait machinery as println! but returns the result as a heap-allocated string.

Use write! inside fmt implementations to build output incrementally. It handles trait bounds and error propagation automatically.

Reach for Debug first. It is the default. Add Display only when you have a specific presentation requirement.

Where to go next