When println! rejects your struct
You build a Point struct for a game coordinate. You want to print it nicely in a log. You write println!("{}", p). The compiler rejects you with E0277 (trait bound not satisfied). It tells you Point doesn't implement std::fmt::Display.
You've probably seen #[derive(Debug)] work instantly. Display is different. It requires you to write the formatting logic yourself. This isn't a bug. It's a feature that forces you to decide what your type actually looks like to a human. The compiler knows how to dump fields for debugging. It doesn't know if you want a date as "2024-05-20" or "May 20, 2024". It doesn't know if a boolean should be "true" or "yes". That choice belongs to you.
Display is for humans, Debug is for machines
Rust separates string representation into two distinct traits. Debug is for developers. It shows the internal structure, field names, and raw values. Display is for end-users. It shows a clean, readable string suitable for UIs, reports, or user-facing logs.
Think of a restaurant. Debug is the kitchen ticket. It lists every ingredient, the cooking temperature, and the prep time. It's precise and useful for the chef. Display is the menu. It says "Grilled Salmon with Lemon Butter". It hides the complexity and presents the value.
You can derive Debug because the compiler sees all the fields. You cannot derive Display because the compiler doesn't know your presentation rules. A Money type might store cents as an integer internally. Debug shows Money { cents: 1500 }. Display should show "$15.00". The compiler can't guess that conversion.
Derive Debug for your sanity. Implement Display for your users.
The minimal implementation
Implementing Display requires an impl block for std::fmt::Display. You define a fmt method that takes a mutable reference to a Formatter and returns a fmt::Result. The write! macro writes to the formatter.
use std::fmt;
/// A 2D coordinate.
struct Point {
x: i32,
y: i32,
}
impl fmt::Display for Point {
// fmt takes a mutable reference to the formatter.
// The return type is fmt::Result, which is just Result<(), fmt::Error>.
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// write! writes to the formatter, not stdout.
// It returns the Result, which we return directly.
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p = Point { x: 10, y: 20 };
// Now this compiles and prints: (10, 20)
println!("{}", p);
}
The write! macro is the key. It looks like println!, but it writes to the Formatter argument f. This allows the caller to control where the output goes. The output might go to a string, a file, or a buffer. Your job is just to provide the content.
Convention: Always use write! inside fmt. Never use println! or print!. Those macros write directly to standard output, bypassing the formatter. If you print directly, the caller's buffer gets nothing, and your output leaks to the console.
How the formatter works
When you call println!("{}", p), the macro expands to call Display::fmt. It creates a Formatter object and passes it to your method. This object holds the formatting specifiers from the format string.
If the user writes {:>10}, the formatter knows the output should be right-aligned and at least 10 characters wide. If they write {:.2}, it knows to use two digits of precision. Your implementation can inspect these settings and adjust the output. If you ignore them, Rust uses defaults. For simple types, ignoring specifiers is usually fine. For types used in tables or reports, respecting them makes your type behave well.
The Formatter provides methods like width(), align(), and precision(). You can check these and format accordingly. There's also a helper method f.pad() that handles width, alignment, and fill characters automatically.
Convention: Check f.width() if your type might appear in formatted tables. Using f.pad() is the cleanest way to support alignment without writing padding logic yourself.
The Formatter is your contract with the caller. Respect the specifiers if you care about alignment.
Realistic example: conditional formatting
Real types often have optional fields or complex logic. You can chain multiple write! calls and use the ? operator to propagate errors. This keeps the code readable and handles formatting failures gracefully.
use std::fmt;
/// A user profile with an optional nickname.
struct Person {
name: String,
nickname: Option<String>,
}
impl fmt::Display for Person {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Start with the name.
// The ? operator propagates errors from write!.
write!(f, "{}", self.name)?;
// Conditionally add the nickname.
// This keeps the output clean when the nickname is absent.
if let Some(nick) = &self.nickname {
write!(f, " (aka {})", nick)?;
}
Ok(())
}
}
fn main() {
let alice = Person {
name: "Alice".to_string(),
nickname: Some("Ali".to_string()),
};
let bob = Person {
name: "Bob".to_string(),
nickname: None,
};
println!("{}", alice); // Alice (aka Ali)
println!("{}", bob); // Bob
}
Chaining write! calls is efficient. Each call writes directly to the formatter's buffer. You avoid creating intermediate strings. The ? operator ensures that if the formatter encounters an error (rare, but possible with custom writers), the error bubbles up immediately.
Convention: Use write! for multiple parts. Chain them with ?. It's cleaner than building a string in memory with format! and then writing that string.
Build the output piece by piece. Let the Formatter handle the buffer management.
Pro tip: respecting alignment
If you want your type to support width and alignment specifiers, use f.pad(). This method takes a string slice and returns a Result. It handles padding, alignment, and fill characters based on the formatter's settings.
use std::fmt;
struct Point {
x: i32,
y: i32,
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Create the base representation.
let s = format!("({}, {})", self.x, self.y);
// f.pad handles width, alignment, and fill character.
// It respects {:>10}, {:<10}, {:^10}, etc.
f.pad(&s)
}
}
fn main() {
let p = Point { x: 1, y: 2 };
// Right-aligned, width 10.
println!("|{:>10}|", p); // | (1, 2)|
// Left-aligned, width 10.
println!("|{:<10}|", p); // |(1, 2) |
}
Using f.pad() makes your type robust. It works correctly in println!("{:>10}", p) and in format strings used by libraries. Without it, your type ignores alignment and breaks table layouts.
Use f.pad() to handle width and alignment. Your type will play nice in tables.
The ToString connection
After implementing Display, you can call .to_string() on your type. You didn't implement ToString. Rust provides a blanket implementation. Any type that implements Display automatically gets ToString.
This is a design choice. It prevents types from having two different string representations. If you need a different string format, make a method like to_csv_string() or to_json_string(). Don't implement ToString manually.
Convention: Never implement ToString manually. Implement Display. The compiler provides the rest. If you see code implementing ToString, it's usually a mistake. The author should have implemented Display instead.
Implement Display. Let the compiler grant you ToString.
Pitfalls and errors
If you implement fmt and return (), the compiler rejects you with E0308 (mismatched types). The signature demands fmt::Result. Even if you never expect an error, return Ok(()). The trait contract requires the result type.
Calling println! inside fmt is a logic bomb. println! writes to standard output immediately. The fmt method must write to the Formatter argument. If you print directly, the caller's buffer gets nothing, and your output leaks to the console. This causes duplicate output or missing data in logs. Always use write!.
Another trap is infinite recursion. If your Display implementation calls println!("{}", self), it calls Display::fmt again, which calls println!, which calls Display::fmt. The stack overflows. This happens when you accidentally use the format string macro on self instead of writing fields. Check your write! arguments carefully.
Write to f, never to stdout. The Formatter is the destination.
Decision matrix
Use Display when you need a human-readable representation for logs, UIs, or files. Use Debug when you need a developer-focused dump of all fields, including private ones. Use #[derive(Debug)] for rapid prototyping and internal logging. Use Display over ToString because implementing Display automatically grants you ToString via a blanket implementation. Reach for write! inside fmt to write to the formatter. Avoid println! or format! inside fmt. Use f.pad() when your type needs to support width and alignment specifiers.
Implement Display once. Get ToString for free. Trust the blanket impl.