How to Use the Display Trait for Custom Formatting
You're building a CLI tool. You have a Task struct with a title, priority, and due date. You want to print a clean list: 1. Buy milk [High]. Instead, you get Task { title: "Buy milk", priority: High, due: ... }. The default debug output is noisy and exposes internal details you don't want the user to see. You need a way to control exactly how your type looks when it's printed.
Rust solves this with the Display trait. Implementing Display gives you full control over the string representation of your type. You decide what fields show up, how they're formatted, and what separators appear. The rest of the Rust ecosystem respects Display automatically. Macros like println!, format!, and write! all look for Display when they see the {} placeholder.
Display versus Debug
Rust provides two standard traits for converting values to text. Debug is for developers. Display is for end users.
Think of Debug like a mechanic's diagnostic report. It lists every sensor reading, every error code, and every internal state exactly as the machine sees it. It's unambiguous and complete. Display is the dashboard gauge. It shows speed, fuel level, and temperature warnings. It presents only what matters, formatted nicely, hiding the noise.
Debug is automatic. You can derive it with #[derive(Debug)]. Display requires manual implementation. Rust won't guess what your user wants to see. You have to define it. This separation keeps user-facing output stable and clean, while preserving a reliable debug dump for development.
Rust won't guess what your user wants to see. You have to define it.
Minimal implementation
The Display trait has one method: fmt. It takes a reference to self and a mutable reference to a Formatter. The Formatter is a buffer that collects the output. You use the write! macro to push text onto it.
use std::fmt;
/// Represents a point in 2D space.
struct Point {
x: f64,
y: f64,
}
// Implement Display to control how Point prints.
impl fmt::Display for Point {
// The fmt method receives a Formatter to write into.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Use write! to format the output.
// The {} placeholders use Display recursively on the fields.
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p = Point { x: 1.0, y: 2.0 };
// {} triggers Display.
println!("Location: {}", p);
}
The write! macro works just like println!, but it writes to the provided formatter instead of stdout. The {} inside write! triggers Display on the fields. If self.x is a f64, Rust uses the built-in Display for floats. The output is (1, 2).
The Formatter is just a buffer with some metadata. Write to it, and the macro handles the rest.
What happens under the hood
When you call println!("{}", p), the macro expands to call the Display trait's fmt method. The compiler checks if Point implements Display. If it does, it passes a mutable reference to a Formatter into your fmt function.
The Formatter holds a reference to an internal buffer. It also carries formatting flags like width, alignment, and precision. These flags come from the format string. If you write {:>10}, the formatter knows the caller requested right alignment with a width of 10. Your fmt implementation can check these flags and adjust the output accordingly.
When fmt returns, the buffer contains the formatted text. The macro then writes that buffer to the destination. If you're using println!, it goes to stdout. If you're using format!, it becomes a String. The mechanism is the same. Display is the contract. The destination is up to the caller.
Generics and trait bounds
When implementing Display for a generic type, you often need to format fields of the generic type. This requires the generic type to implement Display as well. You express this with a trait bound.
use std::fmt;
/// A container holding two values of the same type.
struct Pair<T> {
x: T,
y: T,
}
// Implement Display only if T can be displayed.
impl<T: fmt::Display> fmt::Display for Pair<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Format both fields using their own Display implementations.
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p = Pair { x: 42, y: 99 };
println!("Numbers: {}", p);
// This works because String implements Display.
let s = Pair { x: "hello".to_string(), y: "world".to_string() };
println!("Strings: {}", s);
}
The bound T: fmt::Display tells the compiler that Pair<T> only implements Display when T does. If you try to print a Pair<MyType> where MyType doesn't implement Display, the compiler rejects you with E0277 (the trait bound MyType: std::fmt::Display is not satisfied). This error usually points to the write! call, but the fix is adding the bound to the impl.
Convention aside: When implementing traits for generics, always add the minimal bounds required. Over-constraining with T: fmt::Display + Debug + Clone when you only need Display makes your type harder to use. Stick to what you actually use.
Respecting formatting flags
A robust Display implementation respects formatting flags. The Formatter exposes methods to check width, alignment, and precision. Supporting these flags makes your type feel native to Rust's formatting ecosystem.
use std::fmt;
/// A color in RGB.
struct Color {
r: u8,
g: u8,
b: u8,
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Check if the caller requested a width, e.g., {:>10}.
let width = f.width().unwrap_or(7);
// Build the hex representation.
let hex = format!("#{:02X}{:02X}{:02X}", self.r, self.g, self.b);
// Use the width to pad the output.
// The Formatter handles alignment automatically if you use width$.
write!(f, "{:>width$}", hex, width = width)
}
}
fn main() {
let c = Color { r: 255, g: 0, b: 0 };
println!("Default: {}", c);
println!("Padded: {:>15}", c);
}
The f.width() method returns Option<usize>. If the caller specified a width, it's Some. Otherwise, it's None. You can use unwrap_or to provide a default. The write! macro supports named arguments like width = width, which lets you pass the width dynamically. The alignment flag is handled automatically by the > in the format string.
You can also check f.align() for explicit alignment requests and f.precision() for precision-sensitive types. For a color, precision doesn't apply, but for a float, you might use it to limit decimal places. Checking flags adds a few lines of code but makes your type much more flexible.
Supporting flags makes your type feel native to Rust's formatting ecosystem.
The ToString connection
Implementing Display gives you ToString for free. Rust has a blanket implementation: any type that implements Display automatically implements ToString. This means you get .to_string() and .to_owned() methods without writing extra code.
use std::fmt;
struct Greeting {
name: String,
}
impl fmt::Display for Greeting {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Hello, {}", self.name)
}
}
fn main() {
let g = Greeting { name: "Alice".to_string() };
// .to_string() works because Display is implemented.
let s: String = g.to_string();
println!("{}", s);
}
You rarely need to implement ToString manually. If you find yourself writing impl ToString for MyType, stop. Implement Display instead. It's more general and gives you formatting control. The ToString trait exists mostly for compatibility with older code and for cases where you need a string conversion without the formatting machinery, but Display covers 99% of use cases.
Implement Display once. Get .to_string() for free.
Common pitfalls
The most common error is forgetting the trait bound on a generic type. If you have a struct Wrapper<T> and you try to implement Display using {} on a field of type T, the compiler will complain. You'll see E0277. The fix is adding T: fmt::Display to the impl.
Another pitfall is mixing up Display and Debug. If you implement Display but try to print with {:?}, you'll get a Debug error. The placeholders are strict. {} requires Display. {:?} requires Debug. They don't share implementations. If you want both, derive Debug and implement Display.
Convention aside: Always use write! inside fmt. You can call f.write_str("text") directly, but write! gives you the formatting macros for free. It's the community standard. Using write_str manually is verbose and error-prone. Stick to write!.
Error handling in fmt is rare but possible. The return type is fmt::Result, which is Result<(), fmt::Error>. You can return Err(fmt::Error) if formatting fails. This usually happens only in complex formatters that delegate to other fallible operations. For most types, you just return Ok(()) implicitly via write!.
If the compiler complains about a missing trait bound, check your generics. The error is usually one line up.
When to use Display
Use Display when you're formatting output for a human user, like a CLI report, a web response, or a log message meant for operations. Use Debug when you need a complete, unambiguous dump of the data structure for debugging, testing, or internal logging. Use Display to get ToString for free; implementing Display automatically gives you .to_string() and .to_owned(), so you rarely need to implement ToString manually. Reach for Debug when the type is internal and has no meaningful user representation.
Display is for the user. Debug is for you. Keep them separate.