Formatting: Display and Debug

Every println!, format!, and write! runs on the same machinery: the std::fmt traits. Derive Macros and Common Traits noted that #[derive(Debug)] gives you {:?}, but stopped there. This tutorial goes into the two formatting traits you actually implement, Debug (for developers) and Display (for users), the format-spec mini-language ({:>8.2}, {:#?}, {value:width$}), and how implementing Display unlocks .to_string() for free. Getting formatting right is the difference between readable logs, clean error messages, and polished CLI output versus a wall of default-derived noise.


Debug vs Display: Two Audiences

The two core traits exist for two different readers, and conflating them is the usual mistake:

  • Debug ({:?}) is for programmers: diagnostic output, logs, dbg!, test failures. It should show structure, and you almost always #[derive(Debug)] it.
  • Display ({}) is for end users: the human-facing representation. It is never derived, you implement it by hand, precisely because "how to present this to a user" is a judgment call the compiler can't make.
use std::fmt;

#[derive(Debug)]                       // {:?} — structural, for developers
struct Temperature { celsius: f64 }

impl fmt::Display for Temperature {    // {} — human-facing, hand-written
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:.1}°C", self.celsius)
    }
}

let t = Temperature { celsius: 21.567 };
println!("{t:?}");   // Temperature { celsius: 21.567 }   — Debug
println!("{t}");     // 21.6°C                            — Display

The rule: derive Debug on essentially everything (it costs nothing and helps every log and test), and implement Display only for types with a meaningful human representation. There is intentionally no #[derive(Display)], that's a signal to think about the user-facing form rather than dumping fields.


Implementing Display: Write Into the Formatter

Display::fmt receives a &mut Formatter and returns fmt::Result. You don't build a String, you write! directly into the formatter (which may be a terminal, a file, or a buffer), the same write! from The Read and Write Traits, avoiding an intermediate allocation:

use std::fmt;

struct Point { x: i32, y: i32 }

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)   // writes into f, returns its Result
    }
}

Gotcha: implement Display by writing into the Formatter, never by building a String and returning it, the signature won't even let you (it returns fmt::Result, not String). Use write!(f, ...) and propagate its result with ? if you have multiple writes. A common mistake is calling self.to_string() inside your own Display impl, which recurses infinitely (to_string calls Display). Write the fields directly instead.

The payoff is automatic: implementing Display gives you ToString (and thus .to_string()) for free, via a blanket impl (Generics and Trait Bounds). So point.to_string() and format!("{point}") both work the moment Display exists, no separate to_string method needed. This is also why Display is the right trait for a user-facing error's message (Error Libraries generates it from #[error("...")]).


The Format Spec Mini-Language

The text inside {} after a colon is a compact spec controlling width, alignment, precision, and more. It works in every formatting macro, and learning it replaces a lot of manual string padding:

println!("{:>8}", "hi");       // "      hi"  — right-align, width 8
println!("{:<8}|", "hi");      // "hi      |" — left-align
println!("{:^8}", "hi");       // "   hi   "  — center
println!("{:08.2}", 3.14159);  // "00003.14"  — zero-pad, width 8, 2 decimals
println!("{:+}", 42);          // "+42"       — always show sign
println!("{:#x}", 255);        // "0xff"      — alternate form (hex with prefix)
println!("{:b}", 5);           // "101"       — binary

The grammar is {:[fill][align][sign][#][width][.precision][type]}. The most-used pieces: </>/^ for alignment, a number for minimum width, .N for precision (decimals on floats, max length on strings), and type letters (x/X hex, b binary, o octal, e scientific). Width and precision can also come from arguments: {:width$} or {:.prec$} pulls the value from a named/positional argument, so println!("{:width$}", x, width = 10) sets width dynamically.


Pretty Debug and Named Arguments

Two everyday conveniences round out the picture. The # flag on Debug ({:#?}) switches to pretty-printed, multi-line output, invaluable for inspecting nested structures:

#[derive(Debug)]
struct Config { name: String, retries: u32, tags: Vec<String> }

let c = Config { name: "svc".into(), retries: 3, tags: vec!["a".into(), "b".into()] };

println!("{c:?}");    // Config { name: "svc", retries: 3, tags: ["a", "b"] }  — one line
println!("{c:#?}");   // multi-line, indented — each field on its own line

And since Rust 2021, you can capture variables directly by name in the braces ({c}, {c:#?}) instead of listing them as trailing arguments, cleaner for simple cases:

let name = "Ada";
let score = 95;
println!("{name} scored {score}");          // captures name, score from scope
println!("{score:>5}");                      // capture + format spec together
// trailing-argument form still needed for expressions: println!("{}", a + b);

Inline capture works only for bare variable names, not expressions or field accesses ({self.x} doesn't work, {} with a trailing arg does). Use inline capture for plain locals and the trailing form for anything computed.


Formatting Reference

You wantUse
Developer/diagnostic output#[derive(Debug)] + {:?}
Pretty multi-line debug{:#?}
User-facing representationhand-implement Display + {}
.to_string() on your typeimplement Display (gives ToString free)
Align / pad to a width{:>8} / {:<8} / {:^8}
Fixed decimals{:.2}
Hex / binary / octal{:x} / {:b} / {:o} (add # for prefix)
Width/precision from a variable{:width$} / {:.prec$}
Interpolate a local variable{name} (inline capture)

The throughline: Debug is derived and for developers; Display is hand-written and for users, and both flow through the same Formatter you write! into. The format spec is a small language worth memorizing the common pieces of (>, width, .precision, #), because it eliminates manual padding and rounding throughout your output code.


Key Takeaways

  • Debug ({:?}) is for developers and should be derived on nearly everything; Display ({}) is for end users and is never derived, you hand-write it because presentation is a judgment call.
  • Implement Display::fmt by write!-ing into the &mut Formatter and returning fmt::Result; never build and return a String, and never call self.to_string() inside it (infinite recursion).
  • Implementing Display gives ToString/.to_string() for free via a blanket impl, and is the right trait for user-facing error messages.
  • The format spec {:[fill][align][sign][#][width][.precision][type]} controls alignment (</>/^), width, decimals (.N), sign (+), alternate form (#), and radix (x/b/o); width/precision can come from arguments via $.
  • Use {:#?} for pretty multi-line debug, and inline capture ({name}) for plain local variables (not expressions or field accesses, which still need the trailing-argument form).