Skip to content

Implement custom Debug in Rust

Implement std::fmt::Debug manually when #[derive(Debug)] exposes the wrong representation. The usual struct-shaped implementation uses Formatter::debug_struct(), adds only the fields you want, and finishes the builder.

EXAMPLE: CI-CHECKEDTEST ASSERTED · examples/format-output/src/custom_debug_page.rs#debug-custom-implementation
struct Account {
    username: &'static str,
    api_key: &'static str,
}

impl fmt::Debug for Account {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("Account")
            .field("username", &self.username)
            .field("api_key", &"[redacted]")
            .finish()
    }
}

RustPrint runs this implementation in CI. The test verifies that the visible field is present, the placeholder is present, and the original secret value is absent from the formatted output.

A manual implementation provides one method:

fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result

The formatter is the destination and carries formatting options such as the alternate # flag used by {:#?}.

For most structs, use the formatter’s debug builder rather than manually assembling punctuation. debug_struct() produces the normal struct-shaped Debug representation and supports pretty Debug automatically.

Unlike #[derive(Debug)], a manual implementation does not have to include every field. You decide which calls to .field() are made and what value each field shows.

That is useful when a type contains internal state that is noisy, expensive to inspect, irrelevant to diagnostics, or sensitive. In the checked example, the real API key is deliberately replaced with a constant placeholder.

A custom Debug implementation should not be treated as a security boundary by itself. If a value must never be exposed, also control where it can be logged, copied, serialized, or otherwise inspected.

Rust’s Formatter also provides builders for common Debug shapes:

  • debug_tuple() for tuple-like output
  • debug_list() for sequence-like output
  • debug_set() for set-like output
  • debug_map() for key-value output

These helpers let a manual implementation keep the conventional Rust Debug style instead of recreating brackets and separators yourself.

You can write a completely custom representation

Section titled “You can write a completely custom representation”

If none of the builders match the representation you want, the Debug documentation also permits writing directly to the formatter with write!.

That gives you full control, but it also makes you responsible for the entire representation. Prefer the debug builders when the value is still conceptually a struct, tuple, list, set, or map.

Manual implementations built with the formatter’s debug builders support {:#?}. You therefore do not need separate compact and pretty implementations.

The same stability warning still applies: Debug output is programmer-facing inspection text, not a serialization format or wire protocol. See pretty Debug for that boundary.

The standard library recommends deriving Debug in the ordinary case. Manual Debug is most useful when the derived representation is unavailable or deliberately wrong for diagnostics.

If your problem is simply that the compiler says a type does not implement Debug, start with E0277: type doesn’t implement Debug. If you only want to inspect an ordinary struct, print structs and arrays is the shorter path.