When to derive Debug and when to use custom implementation

Given a type in a library whose internal representation is not exposed. I am always wondering should I "expose" the internal representation via deriving Debug or should I have a custom Debug implementation.

I have looked at some examples in the std and I found two interesting cases that seem to contradict each other:

std::time::Duration is internally represented by secs and nanos integers, but Debug has a custom implementation, so for example

println!("{:?}", std::time::Duration::new(100, 5000));

prints

100.000005s

On the other hand std::time::SystemTime has a platform-dependent internal representation but the Debug for

println!("{:?}", std::time::SystemTime::now());

prints

SystemTime { tv_sec: 1779995532, tv_nsec: 81737297 }

So there it exposes the sec and nanosec integers in some sense. So why have a human readable representation for Duration but a quite similar representation than the internals of Duration for SystemTime? Is there a general rule of thumb when to derive Debug and when to try to make it human readable by a custom implementation?

Derive unless you have a specific reason not to. Almost everything derives and thus leaks internal details in some way, but it's not considered reasonable for others to rely on those details. (And the details are already observable for anything you have the source for, e.g. any crates.io crate.)

Personally the specific reasons I have had are

  • avoiding Debug bounds on generics
  • derive doesn't work (e.g. because a field type didn't apply the previous bullet)
  • sensible error type output for end users due to Termination for Result using Debug (alernatively use anyhow or friends)

I don't know for sure offhand why Duration didn't derive, but at a guess, perhaps because it isn't Display but lacking anything human friendly would be annoying.

Interesting insight. Staying in the domain of time, I looked at a few third-party crates and seem like both I looked at (chrono and jiff) seem to have custom Debug implementations to have human-readable representations, for example: NaiveDateTime in chrono - Rust and DateTime in jiff::civil - Rust.

So in your opinion are those two crates doing it wrong, or would you say this makes sense for them?

One important case is when a derived implementation issues an infinite loop ending by stack overflow. For example:

impl fmt::Debug for GenBlock {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("GenBlock")
            .field("name", &self.name)
            .field("block_type", &self.block_type)
            .field("dir", &self.dir)
            .field("flex", &self.flex)
            .field("out", &self.out)
            .field("vars", &self.vars)
            .field("params", &self.params)
            .field("children", &self.children)
            .field(
                "parent and other",
                &format_args!("<omitted to prevent an infinite loop>"),
            )
            .finish()
    }
}

I excluded several fields in the implementation to prevent the case I mentioned above. I think it's only a case when a custom implementation is critical.

Another reason for a custom Debug is when a type holds a large or shared structure. E.g., a cache or, continuing the time theme, a time zone database. Displaying a list of such things can be needlessly verbose and repetitive, a custom Debug can make the values clearer by not showing useless internal detail. A tz name is much more useful than a table of offsets - I did not check/have no idea whether this is the reason chrono or jiff have custom Debug.

I don't think of I'd call any reasonable implementation wrong, no. You'd have to ask the maintainers about their motivations.

Seems to me that the difference is that SystemTime is dealing with something defined in the Posix standard from 1996. POSIX.1-1996. Which is unlikely to ever change. So it is reasonable for Debug to reflect that standard.