Display vs Debug in Rust: dbg!() & Error Types Guide

Max WellsMax WellsFounder of Rustify

TL;DR: Debug ({:?}) is for developer-facing output and can always be derived with #[derive(Debug)]. Display ({}) is for user-facing output and must be implemented manually; there is no derive for it. All types should derive Debug. Implement Display only when you want to control how a type looks to end users or when defining errors. The dbg!() macro uses Debug; println!("{}") uses Display.


What Are Debug and Display?

Debug and Display are formatting traits that control how a Rust type turns into text, and knowing when to use each one is basic engineering hygiene in 2026.

This page matters most for Rust beginners, backend engineers defining errors, and anyone building CLI or API output that humans will actually read.

#[derive(Debug)]  // gives us {:?} formatting for free
struct Point {
    x: f64,
    y: f64,
}
 
fn main() {
    let p = Point { x: 1.5, y: 2.7 };
 
    println!("{:?}", p);   // Debug:   Point { x: 1.5, y: 2.7 }
    println!("{:#?}", p);  // Pretty-print Debug (indented)
}

Display requires a manual implementation:

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)
    }
}
 
fn main() {
    let p = Point { x: 1.5, y: 2.7 };
    println!("{}", p);   // Display: (1.5, 2.7)
}

Display vs Debug in 2026

Choose Debug for developer-facing inspection in 2026, and choose Display when the output is meant for users, logs, or polished error messages.

Debug ({:?})Display ({})
PurposeDeveloper / machine outputUser-facing output
Derived#[derive(Debug)]❌ Must implement manually
Format specifier{:?} or {:#?}{}
Required bydbg!(), error messagesprintln!("{}"), to_string()
std::error::ErrorOptionalRequired
Typical useLogging, debuggingUser messages, CLI output

If you are unsure, derive Debug by default. Add Display when the text needs to look intentional to another human, especially for custom error types, CLI output, and domain values that appear in logs or APIs.


How Do You Implement Display for Error Types?

Implementing Display for an error type controls what users see. It is required by std::error::Error.

use std::fmt;
 
#[derive(Debug)]
enum AuthError {
    InvalidToken,
    Expired { seconds_ago: u64 },
    InsufficientPermissions { required: String },
}
 
impl fmt::Display for AuthError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AuthError::InvalidToken => write!(f, "invalid or malformed token"),
            AuthError::Expired { seconds_ago } =>
                write!(f, "token expired {seconds_ago}s ago"),
            AuthError::InsufficientPermissions { required } =>
                write!(f, "permission '{required}' is required"),
        }
    }
}
 
impl std::error::Error for AuthError {}

With thiserror, you get Display for free via the #[error("...")] attribute.


When Should You Implement Display?

Implement Display when a type needs a stable, human-friendly string representation, not just because the trait exists.

Good reasons to implement Display include:

  • custom error types shown to users or operators
  • CLI output that should look clean instead of debug-like
  • domain values such as money, status, IDs, or colors
  • types where to_string() should have a clear, canonical meaning

Bad reasons include implementing Display for every internal struct by habit or using Display when you really want richer developer diagnostics from Debug.


How Do You Use dbg!() and When Is It Better Than println!?

dbg!() prints file, line, and value with Debug formatting; and returns the value, so it can be inserted inline.

fn double(x: i32) -> i32 {
    x * 2
}
 
fn main() {
    let x = 5;
    let y = dbg!(x * 2) + 1;  // prints: [src/main.rs:8] x * 2 = 10
    println!("y = {y}");       // prints: y = 11
 
    // dbg! in a chain; doesn't break the expression
    let v = vec![1, 2, 3]
        .iter()
        .map(|&n| dbg!(n * 2))
        .collect::<Vec<_>>();
}

Use dbg!() during development; remove before committing. It prints to stderr, not stdout.


How Do You Implement Both Debug and Display?

Derive Debug and implement Display manually; they are independent traits and do not need to produce the same output.

use std::fmt;
 
#[derive(Debug)]  // developer view: Color { r: 255, g: 0, b: 0 }
struct Color { r: u8, g: u8, b: u8 }
 
impl fmt::Display for Color {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "#{:02X}{:02X}{:02X}", self.r, self.g, self.b)  // user view: #FF0000
    }
}
 
fn main() {
    let red = Color { r: 255, g: 0, b: 0 };
    println!("{:?}", red);  // Color { r: 255, g: 0, b: 0 }
    println!("{}", red);    // #FF0000
    println!("{}", red.to_string());  // #FF0000; Display enables to_string()
}

Implementing Display automatically provides to_string() via the blanket impl in std.


Why Do Display and Debug Matter Professionally?

Display and Debug matter because output quality affects observability, DX, and whether your errors help or waste time in production.

This is not just syntax trivia. Engineers routinely lose time because an error type prints useless debug noise to users, or because a domain type has no clear human-readable form when it reaches logs, CLI output, or support tooling. Knowing when to derive Debug, when to implement Display, and when to use thiserror is part of writing Rust that feels production-ready instead of tutorial-grade.


Frequently Asked Questions

Display represents user-facing output; the Rust standard library cannot know what format you want. Debug has a canonical developer-facing format (Rust syntax), so it can be derived. Display needs your intent.

{:#?} is the "alternate" Debug format; it adds indentation and newlines for nested structures, making large structs and vecs readable. Use it in logs and debug output for complex types.

Yes; implementing Display gives you to_string() automatically via the blanket impl impl<T: Display> ToString for T. No need to implement ToString manually.

fmt::Write is a trait for writing formatted data to a buffer (like String). fmt::Display is for describing how a type should be formatted. They are related but separate; write!(f, ...) inside Display::fmt uses the fmt::Write impl of Formatter.

No. All types should usually derive Debug, but Display should exist only when there is a meaningful user-facing representation.


Sources


  • Derive: #[derive(Debug)] auto-generates Debug
  • Trait: Display and Debug are standard library traits
  • thiserror: Generates Display for error types automatically
  • Error Handling: Display is required for custom errors

Keep Reading

Ready to Land a $80-120k Rust Job?