Skip to content

Debug and Display

Level: 101 → 201 · working knowledge

One line: {} and {:?} are two different traits with two different audiences — Rust will generate the programmer's one for you and refuses to guess the human's, and every default path in the language reaches for the generated one.

Here is where the habit forms:

let x = 11;
println!("{x}");     // 11
println!("{x:?}");   // 11

Identical. Nothing in that program hints that two different traits just ran, so {:?} gets filed away as "the one that always works" — which it very nearly is. Then the same reflex meets a name:

let voter = "Ada";
println!("{voter}");     // Ada
println!("{voter:?}");   // "Ada"

The quotes were not added for emphasis. {:?} asked for the Debug impl, and Debug for a string prints a Rust literal — the thing you would paste back into source, quotes and escapes included. {} asked for Display, which prints the text.


The property, stated once

Two traits, two audiences.

Display, via {} Debug, via {:?}
written for whoever uses the program whoever is writing it
how you get one by hand, always #[derive(Debug)]
for a String the text a Rust literal — quotes, \n, \t, \u{…}
on a Vec, Option, tuple, map does not exist derived from the parts
gives you .to_string(), free assert_eq!, unwrap, dbg!

Debug can be derived because the answer is structural: the type's name, its fields, their names, recursively. Display cannot, because nothing about a type says whether a person wants Ada scored 5/2/0, Ada: 5, 2, 0, or a cell in a table. That is a deliberate omission in the standard library, not a gap someone forgot to fill — and it is the whole reason the rest of this page has anything to say.

Everything below is a consequence.

The compiler suggests {:?}, and it is usually right

You reach for {:?} so often because the container types leave you no choice. Vec, Option, Result, tuples, arrays, HashMap — every one of them has Debug and none of them has Display, for the same reason your own struct needs a hand-written impl: there is no single right way to read a collection out loud. Comma-separated? One per line? With the keys? std declines to decide, so {} on a Vec does not compile:

error[E0277]: `Vec<u8>` doesn't implement `std::fmt::Display`
 --> e0277.rs:3:20
  |
3 |     println!("{}", scores);
  |               --   ^^^^^^ `Vec<u8>` cannot be formatted with the default formatter
  |
  = help: the trait `std::fmt::Display` is not implemented for `Vec<u8>`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

Read that last note carefully, because it is the mechanism by which the habit spreads. For a Vec<u8> it is exactly right — {:?} is what you wanted. But the identical note appears for your own type, where it is the wrong advice:

error[E0277]: `Ballot` doesn't implement `std::fmt::Display`
  |
6 |     println!("{}", b);
  |               --   ^ `Ballot` cannot be formatted with the default formatter
  |
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

The compiler cannot tell the difference between "this type has deliberately declined to have a human form" and "you have not written the human form yet." It offers the same escape hatch for both, and taking it is how Ballot { voter: "Ada", scores: [5, 2, 0] } ends up in front of a voter.

Run it

Verified output of debug_vs_display.rs — regenerated by tools/run_examples.py, never hand-typed.

=== where the habit forms: on an integer, the two agree exactly ===
  {}    on 11i32     -> 11
  {:?}  on 11i32     -> 11
  same characters, so nothing tells you two different traits ran

  {}    on "Ada"     -> Ada
  {:?}  on "Ada"     -> "Ada"
  the quotes are not decoration: Debug for str prints a Rust LITERAL

  {}    on 'A'       -> A
  {:?}  on 'A'       -> 'A'

  {}    on 1.0f64    -> 1
  {:?}  on 1.0f64    -> 1.0   <- Debug keeps the point, so you can
                            see this is a float and not an integer

=== the same trait pair, read the other way round: Debug shows the data ===
  with {:?}       bytes  with {}, bracketed and last on the line
  "Ada"           3      [Ada]
  "Ben "          4      [Ben ]
  ""              0      []
  "Ca\tra"        5      [Ca	ra]
  "O'Neill\n"     8      [O'Neill
]
  "Ada\u{200b}"   6      [Ada​]

  Five of those six names are a bug, and {} renders every one of them as
  something a reviewer would sign off: a trailing space, an empty string, a
  tab, a newline, a zero-width space. The byte count is the tell, and {:?}
  is how you see the cause -- it escapes anything a Rust literal would.

  Note what the O'Neill row did to this table: it wrapped, because the name
  really does contain a newline. That is Display doing its job faithfully,
  and it is why the Debug column had to come first to stay readable.

=== one is generated, one is written -- and that is not an oversight ===
  {}    -> Ada scored 5/2/0
  {:?}  -> Ballot { voter: "Ada", scores: [5, 2, 0] }
  {:#?} ->
      Ballot {
          voter: "Ada",
          scores: [
              5,
              2,
              0,
          ],
      }

  #[derive(Debug)] can be generated because the answer is structural:
  the type's name, its fields, their names. There is no #[derive(Display)]
  in std, because nothing about the type says whether a human wants
  "Ada scored 5/2/0", "Ada: 5, 2, 0", or a row in a table.

=== why you reach for {:?} so often: the containers have no Display ===
  Vec<u8>          {} does not compile   {:?} -> [5, 2, 0]
  Option<u8>       {} does not compile   {:?} -> Some(5)
  Result<u8, E>    {} does not compile   {:?} -> Err(ScoreTooHigh { got: 9, max: 5 })
  (&str, u8, bool) {} does not compile   {:?} -> ("Ada", 5, true)
  BTreeMap         {} does not compile   {:?} -> {"Ada": 5, "Ben": 2}

  Every one of those has a Debug impl and no Display impl, for the same
  reason Ballot needed a hand-written one: there is no single right way
  to read a collection out loud. Comma-separated? One per line? With the
  keys? std declines to guess, so `{}` on a Vec is error E0277.

  Path is the case worth knowing:
    {:?}        -> "/etc/ballots.yaml"
    .display()   -> /etc/ballots.yaml
  A path is not guaranteed to be UTF-8, so Path CANNOT promise the
  lossless string Display implies. `.display()` hands back a helper that
  can -- by admitting it may substitute characters. The missing impl is
  the type telling you something true.

=== Display pays a dividend Debug does not: ToString ===
  b.to_string()      -> "Ada scored 5/2/0"
  format!("{b:?}")   -> "Ballot { voter: \"Ada\", scores: [5, 2, 0] }"
  (both shown with {:?} here, so you can see they really are Strings)

  impl<T: Display> ToString for T is a blanket impl in std, so writing
  Display is what makes .to_string() exist. Debug has no such impl:
  the Debug string is reachable only through format!("{:?}").

  And Debug pays a different one -- it is what the tooling is made of:
    assert_eq!(a, b)   prints both sides on failure   => T: Debug
    .unwrap() on Err   prints the error               => E: Debug
    dbg!(x)            prints file:line and the value => T: Debug
  which is why #[derive(Debug)] belongs on almost everything you write,
  and why leaving it off is felt first by your tests.

=== the delivery paths choose Debug, and they do not ask ===
  your Display sentence   -> score 9 is above the 5 cap
  your derived Debug      -> ScoreTooHigh { got: 9, max: 5 }

  .unwrap()               -> PANIC: called `Result::unwrap()` on an `Err` value: ScoreTooHigh { got: 9, max: 5 }
  .expect("...")          -> PANIC: the ballot file was validated on load: ScoreTooHigh { got: 9, max: 5 }
  fn main() -> Result     -> Error: ScoreTooHigh { got: 9, max: 5 }   (to stderr, exit 1)

  Three of the four use Debug. The one sentence written for a human is
  the one the standard paths never reach -- and none of this is a
  warning, a lint, or a compile error. It is just what gets printed.

=== derived Debug prints every field it can reach ===
  #[derive(Debug)]  -> Session { voter_id: 4711, token: "sk_live_9f3a2b8c1d" }
                       ^ that is now in your log file, your CI output,
                         and the panic message of any failing unwrap

  hand-written      -> Guarded { voter_id: 4711, token: <redacted, 18 chars> }   <- the same two fields
  and it still honours {:#?}, because f.debug_struct() does that for you:
      Guarded {
          voter_id: 4711,
          token: <redacted, 18 chars>,
      }

  The other half of the same rule: Debug output is NOT a stable format.
  std reserves the right to change how any of its types debug-print, so a
  program that parses a {:?} string has built on sand. If you need the
  text to hold still, that is a Display impl -- or serde -- not Debug.

Trap 1: {:?} on text prints a literal — and that cuts both ways

The quotes are the visible half. The escapes are the half that matters — these are rows from the run above, with the one containing a newline left out because it wraps the table (which is itself the point, and the run above shows it happening):

with {:?}       bytes  with {}
"Ada"           3      [Ada]
"Ben "          4      [Ben ]
""              0      []
"Ca\tra"        5      [Ca	ra]
"Ada\u{200b}"   6      [Ada​]

Read the table left-to-right and it is a bug: {:?} put quotes and backslashes into your user-facing output. Read it right-to-left and it is a tool: Display faithfully rendered a trailing space, an empty string, a tab, and a zero-width space as things a reviewer would sign off on, and Debug is what made all four visible. Same pair of traits, opposite jobs — which is why the answer is never "prefer one".

So: {} for anything a person reads, {:?} the moment you are asking what is actually in there. A candidate name that fails to match when it visibly should is a {:?} question every time.

{:#?} is the same Debug impl with the alternate flag set, which the derived impl uses to pretty-print one field per line. It is what you want in a log for anything with more than about three fields.

Trap 2: the panic paths pick Debug, and they do not ask

This is the one worth the page. Write a careful error message for the person who has to fix the ballot file, then count how many of the ways that error can reach them actually print it — the five lines below are lifted from the run above:

your Display sentence   -> score 9 is above the 5 cap
your derived Debug      -> ScoreTooHigh { got: 9, max: 5 }

.unwrap()               -> PANIC: called `Result::unwrap()` on an `Err` value: ScoreTooHigh { got: 9, max: 5 }
.expect("...")          -> PANIC: the ballot file was validated on load: ScoreTooHigh { got: 9, max: 5 }
fn main() -> Result     -> Error: ScoreTooHigh { got: 9, max: 5 }

One in five. unwrap and expect are made of Debug — that is why expect on a Result carries an E: Debug bound that unwrap_or does not — and a failing fn main() -> Result<(), E> prints Error: followed by the Debug form, which is the same fact stated in the aliases lesson — where the two ways out of it are named, and which the kata below prices against each other.

Nothing warns you. There is no lint, no compile error, no clippy note: the sentence you wrote for a human is simply never reached, because the paths that deliver errors by default were all built out of the trait that can be derived. The kata below counts it, fixes it two ways, and prices both fixes.

Trap 3: derived Debug prints every field it can reach

#[derive(Debug)]  -> Session { voter_id: 4711, token: "sk_live_9f3a2b8c1d" }
hand-written      -> Guarded { voter_id: 4711, token: <redacted, 18 chars> }

#[derive(Debug)] is structural, and structural includes the field you did not think of as output — a token, a password, a voter's address. It reaches private fields too, since the derive is generated inside the type's own module. That string is now in your log file, your CI output, and the panic message of every unwrap that touches it.

The fix is to write the impl, and Formatter has a builder for exactly this so you do not lose the pretty-print behaviour:

impl fmt::Debug for Guarded {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Guarded")
            .field("voter_id", &self.voter_id)
            .field("token", &format_args!("<redacted, {} chars>", self.token.len()))
            .finish()
    }
}

The other half of the same rule: Debug output is not a stable format. std reserves the right to change how its own types debug-print, so a program that parses a {:?} string has built on sand. If the text has to hold still — a file format, a wire protocol, a fingerprint — that is a Display impl or serde, and never Debug.

Trap 4: {:<8?} is accepted, and then ignored

Width, alignment and fill are not applied by println!. The macro parses them and hands them to the Formatter as state; it is then the impl that decides whether to look, by calling Formatter::pad. Display for str calls it. Debug for str does not — it writes the opening quote, streams the escaped characters straight out, and writes the closing quote. So {name:<10?} compiles, runs, and does exactly what {name:?} would have done. Nothing warns you: rustc is silent, and so is clippy with pedantic and nursery both on.

Which impls honour the spec is not guessable from the outside, so the program below asks eight of them. The fill is . rather than a space, so that padding is visible where it happens.

debug_ignores_width.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.

fn main() {
    // The fill is '.' so that padding, where it happens, is visible.

    // The bug: a width on {:?} over text is accepted and then ignored.
    println!("[{:.<8?}]  {{:.<8?}} on a &str", "ab");
    // The fix: build the Debug string first, then pad that with Display.
    println!("[{:.<8}]  {{:.<8}} on format!(\"{{:?}}\")", format!("{:?}", "ab"));

    // Width reaches every impl the same way. Only some of them apply it.
    println!("\nsame spec, eight different Debug impls:");
    for (label, shown) in [
        ("i32", format!("[{:.<8?}]", 42)),
        ("f64", format!("[{:.<8?}]", 1.5)),
        ("bool", format!("[{:.<8?}]", true)),
        ("&str", format!("[{:.<8?}]", "ab")),
        ("char", format!("[{:.<8?}]", 'a')),
        ("String", format!("[{:.<8?}]", String::from("ab"))),
        ("Option<u8>", format!("[{:.<8?}]", Some(1u8))),
        ("Vec<u8>", format!("[{:.<8?}]", vec![1u8, 2])),
    ] {
        println!("  {label:<12} {shown}");
    }

    // A container hands its elements the SAME Formatter, so the width lands
    // inside the brackets. Vec<&str> looks untouched only because &str ignores it.
    println!("\ncontainers forward the spec to their elements:");
    println!("  Vec<u8>   [{:.<6?}]", vec![1u8, 2]);
    println!("  Vec<&str> [{:.<6?}]", vec!["a", "b"]);

    // Precision is the same story: it truncates through Display, not Debug.
    println!("\nprecision, on the same eight-character string:");
    println!("  {{:.3}}  [{:.3}]", "abcdefgh");
    println!("  {{:.3?}} [{:.3?}]", "abcdefgh");
}

Verified output of debug_ignores_width.rs — regenerated by tools/run_examples.py, never hand-typed.

["ab"]  {:.<8?} on a &str
["ab"....]  {:.<8} on format!("{:?}")

same spec, eight different Debug impls:
  i32          [42......]
  f64          [1.5.....]
  bool         [true....]
  &str         ["ab"]
  char         ['a']
  String       ["ab"]
  Option<u8>   [Some(1.......)]
  Vec<u8>      [[1......., 2.......]]

containers forward the spec to their elements:
  Vec<u8>   [[1....., 2.....]]
  Vec<&str> [["a", "b"]]

precision, on the same eight-character string:
  {:.3}  [abc]
  {:.3?} ["abcdefgh"]

Three things in that run. Numbers and bool do pad, because their Debug is their Display — which is exactly how the habit forms, since a first attempt on an integer works. Text never pads, on &str, String or char alike. And a container is worse than silent: Vec and Option hand their elements the same Formatter, still carrying your width, so {:.<6?} on vec![1u8, 2] pads the 1 and the 2 instead of the list — while the same spec on Vec<&str> looks untouched only because &str ignores what its container passed down. Precision splits the same way: {:.3} truncates a string, {:.3?} does not.

The fix is to make the thing you want padded a String, so that Display is what does the padding:

// "a,,c"  -> ["a", "", "c"]   — and the arrows line up
println!("{:<7} -> {:?}", format!("{input:?}"), input.split(',').collect::<Vec<&str>>());

That is the idiom every aligned table in the str method pages uses, and this trap is why.

If you are coming from another language

Python. The split is the same one you already have, and the mapping is exact: Display is __str__, Debug is __repr__, {} is f"{x}", {:?} is f"{x!r}". repr("Ada") puts the quotes on for the same reason {:?} does, #[derive(Debug)] is what @dataclass gives you, and {:#?} is roughly pprint. What changes is the fallback, and it changes everything downstream: Python guarantees both — object supplies a default __repr__, and str() falls back to __repr__ when __str__ is absent, so print(anything) works. Rust guarantees neither. No Debug and {:?} fails to compile; no Display and {} fails to compile, with no fallback to the derived form. "Print anything" becomes "print what has said how" — which is why the derive is on almost every type you will ever write, and why a Display impl is a small deliberate act rather than something you get for free.

ABAP. You have met this split as a property of the data element rather than the type: a field's internal form and its output form are different things, and the conversion exit — CONVERSION_EXIT_ALPHA_OUTPUT and friends — is where the human form lives. 0000004711 on the database, 4711 on the screen, and the DDIC decides which one you get. Debug is the internal form and Display is the output form, moved from the data dictionary onto the type as a pair of traits. Two things change. The conversion is now yours to write rather than something the domain carries, so a type with no Display has no output form at all — and rather than silently showing the internal value the way an unconverted WRITE would, the compiler stops the build. And WRITE never had to be told which form you meant, because there was only ever one field; here you choose per call site, every time.

Practice

The error message nobody saw. Write a small TallyError with a derived Debug and a hand-written Display that says something genuinely useful to whoever has to fix the ballot file. Now find every way that error can reach a person — printed with {}, printed with {:?}, through .unwrap(), through .expect(), and out of a fn main() -> Result<(), TallyError> — and count how many carry your sentence.

Then fix it twice. First make Debug delegate to Display and count again; then put the derived Debug back and instead handle the error where it leaves the program. Both reach five out of five, so the interesting question is what each one costs: print the Debug form a failing assert_eq! would show you under each version, and decide which you would rather have at 3am.

Solution

debug_vs_display_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.

//! Kata solution: the error message nobody saw.
//!
//! You wrote a careful sentence for the person who has to fix the ballot file.
//! Then you counted how many of the ways that error can actually reach them
//! print it — and the answer was one out of five. Every other path prints the
//! `Debug` form, because every other path was built out of `Debug`.
//!
//!   rustc --edition 2024 debug_vs_display_kata.rs -o /tmp/dvdk && /tmp/dvdk

use std::fmt;
use std::panic::{self, AssertUnwindSafe};
use std::process::ExitCode;
use std::sync::{Arc, Mutex};

/// The error as most people write it: `Display` for the human, `Debug` derived.
#[derive(Debug)]
enum TallyError {
    ScoreTooHigh { got: u8, max: u8 },
}

impl fmt::Display for TallyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            TallyError::ScoreTooHigh { got, max } => write!(
                f,
                "score {got} is above the {max} cap -- check the ballot file's score column"
            ),
        }
    }
}

impl std::error::Error for TallyError {}

/// The same error with `Debug` written by hand to delegate to `Display`.
///
/// In real code you would write that impl on the error type itself and not
/// derive; the wrapper exists only so both behaviours can run in one program.
/// This is the move `anyhow::Error` makes, and knowing it is a *trade* is the
/// point of the second half of this kata.
struct Delegating(TallyError);

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

impl fmt::Debug for Delegating {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// The sentence we are looking for. A path "delivers" if this survives to it.
const SENTENCE: &str = "score 9 is above the 5 cap";

fn plain() -> Result<u8, TallyError> {
    Err(TallyError::ScoreTooHigh { got: 9, max: 5 })
}

fn delegating() -> Result<u8, Delegating> {
    Err(Delegating(TallyError::ScoreTooHigh { got: 9, max: 5 }))
}

/// Run `f` and hand back the panic message instead of dying, so a panic can be
/// printed as data. Not a pattern to copy.
fn caught<T>(f: impl FnOnce() -> T) -> Result<T, String> {
    let slot: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
    let sink = Arc::clone(&slot);
    let prior = panic::take_hook();
    panic::set_hook(Box::new(move |info| {
        let payload = info.payload();
        let message = payload
            .downcast_ref::<&str>()
            .map(|s| (*s).to_string())
            .or_else(|| payload.downcast_ref::<String>().cloned())
            .unwrap_or_else(|| "<payload was not a string>".to_string());
        *sink.lock().unwrap() = Some(message);
    }));
    let outcome = panic::catch_unwind(AssertUnwindSafe(f));
    panic::set_hook(prior);
    outcome.map_err(|_| slot.lock().unwrap().take().unwrap_or_default())
}

fn panic_text<T>(f: impl FnOnce() -> T) -> String {
    match caught(f) {
        Ok(_) => "<did not panic>".to_string(),
        Err(msg) => msg,
    }
}

/// The five ways the error can reach a person, for one error type.
fn five_paths(display: String, debug: String, unwrap: String, expect: String) -> Vec<(&'static str, String)> {
    vec![
        ("println!(\"{e}\")", display),
        ("println!(\"{e:?}\")", debug),
        (".unwrap()", unwrap),
        (".expect(\"...\")", expect),
        // What the runtime writes to stderr for `fn main() -> Result<(), E>`:
        // `Error: ` followed by the DEBUG form. Reconstructed here because that
        // line goes to stderr and would exit the process.
        ("fn main() -> Result", String::new()),
    ]
}

fn report(title: &str, paths: &[(&'static str, String)]) -> usize {
    println!("\n=== {title} ===");
    let mut delivered = 0;
    for (path, text) in paths {
        let ok = text.contains(SENTENCE);
        if ok {
            delivered += 1;
        }
        println!("  {:<22} {}  {}", path, if ok { "SENTENCE" } else { "   --   " }, text);
    }
    println!("  {delivered} of {} paths delivered the sentence you wrote", paths.len());
    delivered
}

fn main() -> ExitCode {
    println!("The sentence written for the human:");
    println!("  {}", TallyError::ScoreTooHigh { got: 9, max: 5 });

    // ── Part 1: count the paths, with the derived Debug ────────────────────
    let e = TallyError::ScoreTooHigh { got: 9, max: 5 };
    let mut derived = five_paths(
        format!("{e}"),
        format!("{e:?}"),
        panic_text(|| plain().unwrap()),
        panic_text(|| plain().expect("the ballot file was validated on load")),
    );
    derived.last_mut().unwrap().1 = format!("Error: {e:?}");
    let n_derived = report("with #[derive(Debug)] -- what you shipped", &derived);

    // ── Part 2: the same five, with Debug delegating to Display ────────────
    let w = Delegating(TallyError::ScoreTooHigh { got: 9, max: 5 });
    let mut delegated = five_paths(
        format!("{w}"),
        format!("{w:?}"),
        panic_text(|| delegating().unwrap()),
        panic_text(|| delegating().expect("the ballot file was validated on load")),
    );
    delegated.last_mut().unwrap().1 = format!("Error: {w:?}");
    let n_delegated = report("with Debug delegating to Display -- the anyhow move", &delegated);

    // ── Part 3: what that fix cost ─────────────────────────────────────────
    println!("\n=== and what the delegating Debug gave away ===");
    println!("  a failing assert_eq! prints the Debug form of both sides. Compare:");
    println!("    derived     {:?}", TallyError::ScoreTooHigh { got: 9, max: 5 });
    println!("    delegating  {:?}", Delegating(TallyError::ScoreTooHigh { got: 9, max: 5 }));
    println!("  The first names the variant and both fields, which is what you want at");
    println!("  3am with a red test. The second is a sentence -- good for the operator,");
    println!("  and it has quietly removed the field values from your own diagnostics.");

    // ── Part 4: the fix that costs nothing ─────────────────────────────────
    println!("\n=== the other fix: stop letting the runtime choose ===");
    println!("  Keep the derived Debug, and handle the error where it leaves the program:");
    println!();
    println!("      fn main() -> ExitCode {{");
    println!("          match run() {{");
    println!("              Ok(()) => ExitCode::SUCCESS,");
    println!("              Err(e) => {{ eprintln!(\"error: {{e}}\"); ExitCode::FAILURE }}");
    println!("          }}");
    println!("      }}");
    println!();
    let code = match plain() {
        Ok(_) => ExitCode::SUCCESS,
        Err(e) => {
            println!("  what the operator now sees on stderr:  error: {e}");
            println!("  and the process still exits 1:         ExitCode::FAILURE");
            ExitCode::SUCCESS // this demo has to exit 0; a real binary returns FAILURE
        }
    };
    println!("\n  scoreboard: {n_derived} of 5 shipped, {n_delegated} of 5 after delegating,");
    println!("  and 5 of 5 with the derived Debug intact -- because the path that mattered");
    println!("  was never `{{:?}}` at all. It was the one place nobody had written any");
    println!("  printing code, so the runtime picked for you, and it picked Debug.");
    code
}

Verified output of debug_vs_display_kata.rs — regenerated by tools/run_examples.py, never hand-typed.

The sentence written for the human:
  score 9 is above the 5 cap -- check the ballot file's score column

=== with #[derive(Debug)] -- what you shipped ===
  println!("{e}")        SENTENCE  score 9 is above the 5 cap -- check the ballot file's score column
  println!("{e:?}")         --     ScoreTooHigh { got: 9, max: 5 }
  .unwrap()                 --     called `Result::unwrap()` on an `Err` value: ScoreTooHigh { got: 9, max: 5 }
  .expect("...")            --     the ballot file was validated on load: ScoreTooHigh { got: 9, max: 5 }
  fn main() -> Result       --     Error: ScoreTooHigh { got: 9, max: 5 }
  1 of 5 paths delivered the sentence you wrote

=== with Debug delegating to Display -- the anyhow move ===
  println!("{e}")        SENTENCE  score 9 is above the 5 cap -- check the ballot file's score column
  println!("{e:?}")      SENTENCE  score 9 is above the 5 cap -- check the ballot file's score column
  .unwrap()              SENTENCE  called `Result::unwrap()` on an `Err` value: score 9 is above the 5 cap -- check the ballot file's score column
  .expect("...")         SENTENCE  the ballot file was validated on load: score 9 is above the 5 cap -- check the ballot file's score column
  fn main() -> Result    SENTENCE  Error: score 9 is above the 5 cap -- check the ballot file's score column
  5 of 5 paths delivered the sentence you wrote

=== and what the delegating Debug gave away ===
  a failing assert_eq! prints the Debug form of both sides. Compare:
    derived     ScoreTooHigh { got: 9, max: 5 }
    delegating  score 9 is above the 5 cap -- check the ballot file's score column
  The first names the variant and both fields, which is what you want at
  3am with a red test. The second is a sentence -- good for the operator,
  and it has quietly removed the field values from your own diagnostics.

=== the other fix: stop letting the runtime choose ===
  Keep the derived Debug, and handle the error where it leaves the program:

      fn main() -> ExitCode {
          match run() {
              Ok(()) => ExitCode::SUCCESS,
              Err(e) => { eprintln!("error: {e}"); ExitCode::FAILURE }
          }
      }

  what the operator now sees on stderr:  error: score 9 is above the 5 cap -- check the ballot file's score column
  and the process still exits 1:         ExitCode::FAILURE

  scoreboard: 1 of 5 shipped, 5 of 5 after delegating,
  and 5 of 5 with the derived Debug intact -- because the path that mattered
  was never `{:?}` at all. It was the one place nobody had written any
  printing code, so the runtime picked for you, and it picked Debug.

See also

  • What dbg! does — the macro that needs the Debug above, and the five things it does that println!("{:?}") does not, starting with handing your value back
  • When a struct refuses — the two E0277s on this page, beside the six other struct errors that share their first week
  • expect — where the E: Debug bound comes from, and why the message is the claim rather than the complaint
  • The Result you are reading is probably an aliasfmt::Result is the return type of every impl on this page, and step 4 there is this page's Trap 2 seen from the Result side
  • Option vs Result — where a custom error type with a Display impl comes from in the first place
  • What a panic costs — the delivery path Trap 2 is about, priced
  • What a float actually stores — why {} prints 1 and {:?} prints 1.0 for the same f64: Debug keeps the decimal point so you can see it is not an integer
  • The std::fmt module docs ↗ — the full grammar of a format string: width, precision, fill, sign, and the other traits (LowerHex, Binary, Pointer) the same syntax reaches

Po polsku

Dwie cechy (traits), dwie grupy odbiorców: {} sięga po Display — formę dla człowieka, który używa programu — a {:?} po Debug, czyli formę dla tego, kto ten program pisze. Debug da się wygenerować przez #[derive(Debug)], bo odpowiedź jest strukturalna: nazwa typu, nazwy pól, rekurencyjnie. Display wygenerować się nie da i w std nie ma żadnego #[derive(Display)] — to celowa dziura, nie przeoczenie, bo nic w typie nie mówi, czy człowiek chce zobaczyć Ada scored 5/2/0, Ada: 5, 2, 0, czy komórkę tabeli. Zły nawyk bierze się stąd, że na liczbie obie formy dają identyczny wynik (11 i 11), więc {:?} zapisuje się w pamięci jako „ten, który zawsze działa” — a potem to samo trafia na tekst.

Dla polskiego czytelnika najważniejsza jest wiadomość, której strona wprost nie mówi: {:?} na tekście wypisuje literał Rusta, ale polskich znaków nie ucieka. println!("{:?}", "Zażółć gęślą jaźń") daje "Zażółć gęślą jaźń" w cudzysłowie, a nie ciąg \u{17c} — uciekane jest tylko to, czego i tak nie widać: tabulator, znak nowej linii, spacja zerowej szerokości jako \u{200b}. I to jest dokładnie zastosowanie tej cechy: kiedy nazwa kandydata „widocznie” pasuje, a porównanie zwraca false, pytanie brzmi {:?}, nie {}. Uwaga na kolumnę bajtów w tabeli powyżej — .len() zwraca bajty, więc "Zażółć gęślą jaźń".len() to 26 przy 17 znakach; sama „za duża” liczba bajtów nie jest jeszcze dowodem błędu w polskim tekście, dowodem jest to, co pokaże {:?}.

Pułapka 2 jest tą, dla której warto przeczytać całą stronę. Piszesz staranne zdanie po ludzku w Display, a potem liczysz, ile dróg dostarcza je do odbiorcy: unwrap, expect i fn main() -> Result wypisują Debug, więc wynik to 1 z 5 — i nic o tym nie ostrzega, ani kompilator, ani clippy. Dwa wyjścia mają różną cenę. Można napisać Debug ręcznie tak, żeby delegował do Display (ruch, który robi anyhow) — wtedy 5 z 5, ale nieudany assert_eq! przestaje pokazywać wartości pól, czyli oddajesz własną diagnostykę. Albo zostawić wygenerowany Debug i obsłużyć błąd tam, gdzie wychodzi z programu (match run() { Err(e) => eprintln!("error: {e}") }) — też 5 z 5, bez żadnej straty.

Dwie rzeczy na koniec, obie zaskakują. #[derive(Debug)] wypisuje każde pole, do którego sięgnie, łącznie z prywatnymi — więc token czy hasło ląduje w logu, w wyjściu CI i w komunikacie każdej paniki; ratunkiem jest ręczny impl z f.debug_struct(...), który zachowuje też {:#?}. I szerokość: {:.<12?} na tekście kompiluje się, uruchamia i nic nie robi — daje ["łąka"], bo Debug for str nie woła Formatter::pad. Wyrównanie trzeba zrobić przez Display na gotowym łańcuchu znaków: format!("{:.<12}", format!("{x:?}")) daje ["łąka"......], a dopełnienie liczy wtedy znaki, a nie bajty — co przy polskich literach jest akurat tym, czego się chce.

Szukaj po polsku: cecha Debug i Display · formatowanie tekstu w Ruscie · rust Debug vs Display · rust derive Display · rust E0277 doesn't implement Display