Process errors in `fmt`: what is the right way?

I implement Debug as

    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match from_utf8(&self.data) { //Some u8 array
            Ok(str) => { f.write_str(str) }
            Err(err) => {
                // We have junk in this array which is not a valid UTF-8
               // What should I do with 'err' here?
                Err(std::fmt::Error::default())
            }
        }
    }

I wish I could return Utf8Error somehow, so println!("{foo:?}") would panic telling client what happened.

However, docs say

/// This type does not support transmission of an error other than that an error
/// occurred. Any extra information must be arranged to be transmitted through
/// some other means.

That means, I can't attach Utf8Error forcing caller to guess why did my struct failed to print debug info.

  1. Why doesn't fmt::Error have reason field?
  2. What is the right thing to do here?

I am aware of from_utf8_lossy and other things, so I could return junk in case of error, but I want to emphasize that I failed to convert array to string.

Or do I misunderstand the idea of fmt? Maybe it should never fail, and error is here only for cases like broken formatter (i.e trying to print to the closed descriptor etc)?

I generally think that returning errors from fmt is not a good idea. Instead, I would just emit something that indicates that you couldn't format it.

For example, here is how the Tokio mutex implements it:

impl<T: ?Sized> std::fmt::Debug for Mutex<T>
where
    T: std::fmt::Debug,
{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut d = f.debug_struct("Mutex");
        match self.try_lock() {
            Ok(inner) => d.field("data", &&*inner),
            Err(_) => d.field("data", &format_args!("<locked>")),
        };
        d.finish()
    }
}

This way, we either print the inner value, or if we are unable to, we just emit <locked>. You could do something similar:

fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
    match from_utf8(&self.data) { //Some u8 array
        Ok(str) => f.write_str(str),
        Err(err) => f.write_str("<invalid utf-8>"),
    }
}

If you really want to return an error, well, as the docs say, you can't emit more details than "something went wrong".

3 Likes

You can also print non-utf8 bytes in some kind of encoded version (like hex-encoded). For inspiration, look at what OsString and Path from std do.

Yes. The purpose of fmt::Error existing at all is to allow a formatting operation to be cancelled midway by its fmt::Write destination, such as on an IO error. The fmt implementations themselves are supposed to be infallible. This is unfortunately not very prominently documented; it's only mentioned in the fmt module documentation:

However, they should never return errors spuriously. That is, a formatting implementation must and may only return an error if the passed-in Formatter returns an error. This is because, contrary to what the function signature might suggest, string formatting is an infallible operation. This function only returns a result because writing to the underlying stream might fail and it must provide a way to propagate the fact that an error has occurred back up the stack.

What to do instead?

  • For fmt::Debug in particular, you might prefer to err on the side of printing no matter what, and use from_utf8_lossy or similar, or even an explicit printed “I'm broken” report — for more use in debugging.

  • But for fmt::Display or other "actual output of your program" formatting, if you encounter a bug such as a broken invariant in your own data type (if the bytes should always be UTF-8 and if they aren't, a constructor failed to do its job of checking), then just panic!("...") for that.

  • If there is some condition where your type might or might not be printable, then check the condition before producing a Display implementation.

    impl MyType {
        fn display(&self) -> Result<impl fmt::Display + '_, Utf8Error> {
            ...
        }
    }
    
9 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.