Rust Display and Debug Traits: Formatting

Rust's formatting system centers around two fundamental traits: `Debug` and `Display`. These traits define how your types convert to strings, but they serve distinctly different purposes. `Debug`...

Key Insights

  • Debug is for developers and can be auto-derived with #[derive(Debug)], while Display is for end users and must be manually implemented to control the public-facing representation
  • The {:?} and {:#?} format specifiers provide compact and pretty-printed Debug output respectively, essential for rapid debugging of complex nested structures
  • Display implementations integrate directly with Rust’s error handling system, making well-formatted Display traits critical for maintainable error messages throughout your application

Introduction to Formatting Traits

Rust’s formatting system centers around two fundamental traits: Debug and Display. These traits define how your types convert to strings, but they serve distinctly different purposes. Debug provides developer-facing output optimized for debugging and inspection, while Display offers user-facing output that should be carefully crafted for production use.

Every time you use println!, format!, or any string formatting macro, you’re invoking one of these traits. Understanding when and how to implement them is essential for writing idiomatic Rust code.

Here’s what happens when you try to print a struct without implementing either trait:

struct User {
    id: u32,
    username: String,
}

fn main() {
    let user = User {
        id: 1,
        username: String::from("alice"),
    };
    
    // This won't compile: User doesn't implement Display
    // println!("{}", user);
    
    // This won't compile either: User doesn't implement Debug
    // println!("{:?}", user);
}

The compiler will reject both attempts with clear error messages indicating which trait is missing. Let’s fix this.

The Debug Trait

The Debug trait exists for debugging and development. It should provide a complete, unambiguous representation of your type’s internal state. For most types, you can automatically derive Debug:

#[derive(Debug)]
struct User {
    id: u32,
    username: String,
    email: Option<String>,
}

fn main() {
    let user = User {
        id: 1,
        username: String::from("alice"),
        email: Some(String::from("alice@example.com")),
    };
    
    // Compact debug output
    println!("{:?}", user);
    // Output: User { id: 1, username: "alice", email: Some("alice@example.com") }
    
    // Pretty-printed debug output
    println!("{:#?}", user);
    /* Output:
    User {
        id: 1,
        username: "alice",
        email: Some(
            "alice@example.com",
        ),
    }
    */
}

The {:#?} specifier is invaluable when debugging deeply nested structures. It adds indentation and line breaks, making complex data structures readable at a glance.

Sometimes you need custom Debug implementations for sensitive data or to improve clarity:

use std::fmt;

struct Password(String);

impl fmt::Debug for Password {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Password([REDACTED])")
    }
}

#[derive(Debug)]
struct Credentials {
    username: String,
    password: Password,
}

fn main() {
    let creds = Credentials {
        username: String::from("alice"),
        password: Password(String::from("super_secret")),
    };
    
    println!("{:?}", creds);
    // Output: Credentials { username: "alice", password: Password([REDACTED]) }
}

This pattern prevents accidentally logging sensitive information while maintaining useful debug output.

The Display Trait

The Display trait is for user-facing output. It cannot be auto-derived because you must make deliberate decisions about how your type appears to end users. Implement Display when your type has a natural, human-readable representation.

Here’s a basic Display implementation:

use std::fmt;

struct User {
    id: u32,
    username: String,
}

impl fmt::Display for User {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "User #{}: {}", self.id, self.username)
    }
}

fn main() {
    let user = User {
        id: 1,
        username: String::from("alice"),
    };
    
    println!("{}", user);
    // Output: User #1: alice
}

For enums, use pattern matching to provide appropriate representations:

use std::fmt;

enum Status {
    Active,
    Inactive,
    Suspended { reason: String },
}

impl fmt::Display for Status {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Status::Active => write!(f, "active"),
            Status::Inactive => write!(f, "inactive"),
            Status::Suspended { reason } => write!(f, "suspended: {}", reason),
        }
    }
}

fn main() {
    let status = Status::Suspended {
        reason: String::from("payment overdue"),
    };
    
    println!("Account status: {}", status);
    // Output: Account status: suspended: payment overdue
}

When working with nested types, Display implementations can build on each other:

use std::fmt;

struct Point {
    x: f64,
    y: f64,
}

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

struct Line {
    start: Point,
    end: Point,
}

impl fmt::Display for Line {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} -> {}", self.start, self.end)
    }
}

fn main() {
    let line = Line {
        start: Point { x: 0.0, y: 0.0 },
        end: Point { x: 10.0, y: 5.0 },
    };
    
    println!("{}", line);
    // Output: (0, 5) -> (10, 5)
}

Advanced Formatting Options

Rust’s formatting system supports sophisticated layout control through format specifiers. These work with both Debug and Display implementations.

Width and alignment control how values fit into fixed-width fields:

fn main() {
    let name = "Alice";
    
    // Right-aligned in 10 characters
    println!("|{:>10}|", name);
    // Output: |     Alice|
    
    // Left-aligned in 10 characters
    println!("|{:<10}|", name);
    // Output: |Alice     |
    
    // Center-aligned in 10 characters
    println!("|{:^10}|", name);
    // Output: |  Alice   |
    
    // Custom fill character
    println!("|{:*^10}|", name);
    // Output: |**Alice***|
}

Precision controls decimal places for floats and truncation for strings:

fn main() {
    let pi = 3.14159265359;
    
    // Two decimal places
    println!("{:.2}", pi);
    // Output: 3.14
    
    // Six decimal places
    println!("{:.6}", pi);
    // Output: 3.141593
    
    let text = "Hello, world!";
    
    // Truncate to 5 characters
    println!("{:.5}", text);
    // Output: Hello
}

Combine multiple specifiers for complex formatting:

fn main() {
    let price = 42.7;
    
    // Right-aligned, 10 characters wide, 2 decimal places
    println!("Price: ${:>10.2}", price);
    // Output: Price: $     42.70
    
    // Zero-padded numbers
    println!("ID: {:05}", 42);
    // Output: ID: 00042
}

Error Handling and Display

Display implementations are crucial for error handling in Rust. The standard library’s Error trait requires Display, making well-formatted error messages a first-class concern:

use std::fmt;
use std::error::Error;

#[derive(Debug)]
enum DatabaseError {
    ConnectionFailed(String),
    QueryFailed { query: String, reason: String },
    RecordNotFound(u32),
}

impl fmt::Display for DatabaseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DatabaseError::ConnectionFailed(host) => {
                write!(f, "Failed to connect to database at {}", host)
            }
            DatabaseError::QueryFailed { query, reason } => {
                write!(f, "Query failed: '{}' (reason: {})", query, reason)
            }
            DatabaseError::RecordNotFound(id) => {
                write!(f, "Record with ID {} not found", id)
            }
        }
    }
}

impl Error for DatabaseError {}

fn fetch_user(id: u32) -> Result<String, DatabaseError> {
    if id == 0 {
        Err(DatabaseError::RecordNotFound(id))
    } else {
        Ok(String::from("alice"))
    }
}

fn main() {
    match fetch_user(0) {
        Ok(user) => println!("Found user: {}", user),
        Err(e) => eprintln!("Error: {}", e),
    }
    // Output: Error: Record with ID 0 not found
}

This pattern integrates seamlessly with the ? operator, propagating well-formatted errors up the call stack:

fn process_user(id: u32) -> Result<(), DatabaseError> {
    let user = fetch_user(id)?;
    println!("Processing user: {}", user);
    Ok(())
}

Best Practices and Common Patterns

Always implement Debug for your types, even if you don’t think you’ll need it. Use #[derive(Debug)] unless you have specific reasons to customize it. Debug output should be comprehensive—include all fields that might be relevant during debugging.

Implement Display only when your type has a natural, user-facing representation. Not every type needs Display. Internal implementation details, intermediate data structures, and types that exist purely for code organization often don’t benefit from Display implementations.

The newtype pattern frequently requires forwarding implementations:

use std::fmt;

struct UserId(u32);

impl fmt::Display for UserId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Forward to the inner type's Display
        write!(f, "{}", self.0)
    }
}

impl fmt::Debug for UserId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Provide semantic context in debug output
        write!(f, "UserId({})", self.0)
    }
}

Sometimes formatting should vary based on internal state:

use std::fmt;

struct Counter {
    value: u32,
    max: u32,
}

impl fmt::Display for Counter {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}/{}", self.value, self.max)?;
        
        if self.value >= self.max {
            write!(f, " (FULL)")
        } else if self.value == 0 {
            write!(f, " (EMPTY)")
        } else {
            Ok(())
        }
    }
}

fn main() {
    let counter = Counter { value: 10, max: 10 };
    println!("{}", counter);
    // Output: 10/10 (FULL)
}

Master these traits and you’ll write more maintainable Rust code with clear debugging output and polished user-facing messages. The distinction between Debug and Display isn’t just a technical detail—it’s a design philosophy that separates development concerns from production requirements.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.