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?