Skip to content

← Shape Your Data: Structs, Enums, Pattern Matching step 18 of 26

Easy Primitives

#[derive(Debug)] and the three shapes of debug output

Render one of three structs with either {:?} or {:#?}, and return the result byte for byte.

pub fn render(kind: String, a: i64, b: String) -> String
kind output
unit Unit
unit# Unit
tuple Tup(1, "x")
tuple# three lines: Tup(, indented fields, )
named Named { count: 1, label: "x" }
named# multi-line, one field per line
anything else ?

Tup is Tup(i64, String) built from (a, b); Named is Named { count: a, label: b }. The tests pin the exact bytes, so get the spacing right — or rather, let the derive get it right for you.

{:?} is a trait, not magic

println!("{}", x) uses Display. println!("{:?}", x) uses Debug. Both are ordinary traits, and a type has them only if someone implemented them. The first hard error most beginners hit is:

error[E0277]: `Named` doesn't implement `Debug`
note: add `#[derive(Debug)]` to `Named` or manually `impl Debug for Named`

The starter ships it. The fix is one attribute, and — this is the part worth internalising — #[derive(Debug)] requires every field to be Debug too. Derive generates impl Debug for Named whose body formats each field, so a single non-Debug field poisons the whole struct. In practice almost everything in std is Debug, so the fix is usually to add the attribute further down your own type tree.

Display is deliberately not derivable

There is no #[derive(Display)] and there never will be. Debug can be generated because there is one obvious answer: print the type’s name and its fields. Display is the human-facing rendering, and there is no obvious answer — should a Duration show as 3s, 00:00:03 or 3000ms? std makes you decide, by writing impl fmt::Display by hand.

Rule of thumb: Debug is for programmers, Display is for users. If a string is going in a log line or an error message you are debugging, {:?} is right and is not laziness.

The three shapes, and {:#?}

Derived Debug mirrors the way you declared the type:

struct Unit;                  ->  Unit
struct Tup(i64, String);      ->  Tup(1, "x")
struct Named { count, label } ->  Named { count: 1, label: "x" }

The # flag turns on alternate formatting — the pretty, one-field-per-line form with four-space indentation and a trailing comma on every field. It nests, which is what makes it the right tool for a deeply structured value. A unit struct has nothing to expand, so {:?} and {:#?} agree on it.

Note also that Debug for String prints the quotes and the escapes: a string containing a newline shows as "a\nb", not as two lines. That is the point — Debug is unambiguous where Display is readable.

One caveat about pinning Debug output

Pinning exact Debug bytes is fine here because these are your types and you control the derive. Do not do it against std types. The standard library explicitly does not guarantee the stability of Debug output: the representation of HashMap, Duration, Path and friends can change between releases without it being a breaking change. Asserting on them makes a test that fails on a toolchain upgrade for no reason.

Two allow-by-default lints in the area

missing_fields_in_debug catches a hand-written Debug that quietly skips a field — often how a secret ends up invisible in one log line and visible in another. empty_structs_with_brackets prefers struct Unit; over struct Unit {}.

Remember the grade is compile + tests + clippy -D warnings.