🎓 A1Learner
🔍

Rust Debug Builders

Hand-written Debug is a Debug impl written by hand with builder tools. Derived Debug shows every field exactly as stored.

Build the view piece by piece

f.debug_struct names the type and adds the fields one at a time. Written out in full, it prints exactly what the derive would print:

f.debug_struct("Account")
    .field("owner", &self.owner)
    .field("balance", &self.balance)
    .field("pin", &self.pin)
    .finish()

Hide what should stay hidden

Swap the real value for a mask, and the secret never reaches the log. This is the main reason to write Debug by hand:

.field("pin", &"****")

Tuple structs use debug_tuple

A tuple struct has no field names, so its builder takes the values in order:

f.debug_tuple("Pair").field(&self.0).field(&self.1).finish()

Leave the rest unsaid

finish_non_exhaustive ends the view with .. It is the honest way to say the type holds more than you are showing:

f.debug_struct("Config")
    .field("retries", &self.retries)
    .finish_non_exhaustive()

Alignment needs a String first

Debug does not promise to honour alignment. Format the value into a String first, then pad the text:

println!("[{:^13}]", format!("{:?}", Some(7)));

Debug in hex

The x? form asks Debug to print its numbers in hex — made for byte buffers and bit patterns. X? is the uppercase twin:

println!("{:x?} {:X?}", vec![255, 16], vec![255, 16]);

Example

use std::fmt;

struct Account {
    owner: String,
    balance: u32,
    pin: u32,
}

impl fmt::Debug for Account {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Account")
            .field("owner", &self.owner)
            .field("balance", &self.balance)
            .field("pin", &"****")
            .finish()
    }
}

struct Pair(i32, i32);

impl fmt::Debug for Pair {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("Pair").field(&self.0).field(&self.1).finish()
    }
}

fn main() {
    let acc = Account { owner: String::from("Ana"), balance: 250, pin: 1234 };
    println!("log line: {acc:?}");
    println!("pretty:\n{acc:#?}");
    println!("point: {:?}", Pair(3, 4));
}

Key points

  • Derive Debug by default: it is faithful and free, and every public type is expected to have a Debug form
  • Write Debug by hand for three reasons.
  • Hide a secret.
  • Rename or reformat a field.
  • Or show a computed value instead of the stored one.
  • The builder family covers every shape: debug_struct, debug_tuple, debug_list, debug_set and debug_map
  • Pretty printing comes free with builders: they check the # flag themselves, so {:#?} spreads a hand-written Debug over lines just like a derived one
  • Masking a password protects your users.
  • Making a value look like a different value sends a future bug hunt in the wrong direction.
  • Writing Debug by hand means building the printed view yourself with the builder tools.

In this example, Account stores a real pin, 1234. But its hand-written Debug never shows it. The builder is handed the text "****" for the pin field. The log line prints the account on one line with the pin masked. The pretty print uses {:#?} and spreads the same view over five lines — the builder handled the # flag by itself, with no extra code. Pair shows the tuple builder: no field names, just the two values in order, printing Pair(3, 4). Replace the two impl blocks with #[derive(Debug)]. The pin would then print in full. That leak is what the hand-written version exists to prevent.

Note

Keep a hand-written Debug faithful.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Your struct holds a password, and the logs keep printing it. What is the right fix?