Yet another blog restart

24 Jun 2026

my paper cuts with rustfmt - and what to do about them

rustfmt occasionally fails me. Of cause, the examples are somewhat taken from production code, but with all identifiers rendered unrecognizable and all strings replaced by random words. Unhelpful comments to all of this would be “why don’t you use shorter identifiers?” or “use shorter error messages”. But let’s face it:

Setup

For this post, I’m using rustfmt. That’s not the only tool for formatting rust files. In a productive setting you probably use some sort of editor/ide integration (which calls rustfmt itself) or an LSP (which probably also just makes a syscall to rustfmt) or use the higher level cargo tool cargo fmt (which also calls rustfmt, but for the entire crate or workspace, so way more convenient as git hook or pipeline stage).

Here’s the initial code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/// original version
use some_crate_we_are_dropping::legacy_function;   
use some_upstream_crate::{some_loop, CustomThiserrorEnumType};   
  
#[deprecated]   
pub async fn handle_some_rare_edge_case_for_now_aye() -> Result<(), CustomThiserrorEnumType> {   
    some_high_level_function(|| {
        match (crate::some_other_mod::modern_function(), legacy_function()) {
            (None, None) => Ok(None),
            (Some(x), None) | (None, Some(x)) => Ok(Some(x)),
            (Some(x), Some(y)) if x == y => Ok(Some(x)),
            (Some(x), Some(y)) => Err(CustomThiserrorEnumType::OneVariant(
                AAAAAAAAAErrorEnum::OtherError(anyhow::anyhow!(
                "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lore"
                                )),
            )),                       
        }         
    })        
    .await    

Doesn’t look too bad. Creating an anyhow error, only to instantiate a thiserror error with it, only to instantiate another thiserror error with it is probably a code smell. But you see that we’re in the transition from an old API to a new API. We need to return a Result<(), CustomThiserrorEnumType>, refactoring the error type and all usage sites would be out of scope.

What’s odd is the indentation of the first )),. But rustfmt --edition 2021 leaves the code as-is.

Curiously, I was at first blaming the type names. Because once I added some use, the formatting improves:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/// rustfmt output after manual edit
use some_crate_we_are_dropping::legacy_function;   
use some_upstream_crate::{some_loop, CustomThiserrorEnumType};   
  
#[deprecated]   
pub async fn handle_some_rare_edge_case_for_now_aye() -> Result<(), CustomThiserrorEnumType> {   
    some_high_level_function(|| {
        use AAAAAAAAAErrorEnum::OtherError;
        use CustomThiserrorEnumType::OneVariant;
            
        match (crate::some_other_mod::modern_function(), legacy_function()) {
            (None, None) => Ok(None),
            (Some(x), None) | (None, Some(x)) => Ok(Some(x)),
            (Some(x), Some(y)) if x == y => Ok(Some(x)),
            (Some(x), Some(y)) => Err(OneVariant(OtherError(anyhow::anyhow!(
                "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lore"
            )))), 
        }     
    })
    .await    
}

Now the lines around the enums got rewrapped and parentheses merged.

Curiously, one can also wrap the string:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/// manual edit as rustfmt input
use some_crate_we_are_dropping::legacy_function;   
use some_upstream_crate::{some_loop, CustomThiserrorEnumType};   
  
#[deprecated]   
pub async fn handle_some_rare_edge_case_for_now_aye() -> Result<(), CustomThiserrorEnumType> {   
    some_high_level_function(|| {   
        match (crate::some_other_mod::modern_function(), legacy_function()) {   
            (None, None) => Ok(None),   
            (Some(x), None) | (None, Some(x)) => Ok(Some(x)),   
            (Some(x), Some(y)) if x == y => Ok(Some(x)),   
            (Some(x), Some(y)) => Err(CustomThiserrorEnumType::OneVariant(   
                AAAAAAAAAErrorEnum::OtherError(anyhow::anyhow!(   
        "Lorem Ipsum is simply dummy text of the printing and typesetting industry. \   
        Lore"   
                                )),   
            )),   
        }   
    })   
    .await   
}   

and rustfmt will improve the indentations

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/// rustfmt output after manual edit
use some_crate_we_are_dropping::legacy_function;   
use some_upstream_crate::{some_loop, CustomThiserrorEnumType};   
  
#[deprecated]   
pub async fn handle_some_rare_edge_case_for_now_aye() -> Result<(), CustomThiserrorEnumType> {   
    some_high_level_function(|| {   
        match (crate::some_other_mod::modern_function(), legacy_function()) {   
            (None, None) => Ok(None),   
            (Some(x), None) | (None, Some(x)) => Ok(Some(x)),   
            (Some(x), Some(y)) if x == y => Ok(Some(x)),   
            (Some(x), Some(y)) => Err(CustomThiserrorEnumType::OneVariant(   
                AAAAAAAAAErrorEnum::OtherError(anyhow::anyhow!(   
                    "Lorem Ipsum is simply dummy text of the printing and typesetting industry. \
        Lore"   
                )),                   
            )),   
        }   
    })   
    .await   
}   

Except, curiously, rustfmt only indents the first line of a multi-line string. And I consider this the danger-zone. In the printout, I wouldn’t want to see many white spaces between the period and Lore. But that’s fine, rust ignores the leading spaces, I can indent the second line all I want and rustfmt won’t move Lore back to the 9th column.

Going back to the initial example, when calling

/home/pseyfert/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/rustfmt --error-on-unformatted --unstable-features --edition 2021 example/nightly.rs --config error_on_line_overflow=true,format_strings=true

we get finally something acceptable

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/// rustfmt output after manual edit
use some_crate_we_are_dropping::legacy_function;
use some_upstream_crate::{some_loop, CustomThiserrorEnumType};

#[deprecated]
pub async fn handle_some_rare_edge_case_for_now_aye() -> Result<(), CustomThiserrorEnumType> {
    some_high_level_function(|| {
        match (crate::some_other_mod::modern_function(), legacy_function()) {
            (None, None) => Ok(None),
            (Some(x), None) | (None, Some(x)) => Ok(Some(x)),
            (Some(x), Some(y)) if x == y => Ok(Some(x)),
            (Some(x), Some(y)) => Err(CustomThiserrorEnumType::OneVariant(
                AAAAAAAAAErrorEnum::OtherError(anyhow::anyhow!(
                    "Lorem Ipsum is simply dummy text of the printing and typesetting industry. \
                     Lore"
                )),
            )),
        }
    })
    .await
}

Note, here I let the formatter wrap the multi line string.

Aren’t configurable formatters bad?

Yes. It’s important to note though that the output of my configured nightly rustfmt is invariant under the standard stable rustfmt.

The output of the configured nightly rustfmt is in the fixpoint set of standard stable rustfmt.

Does it matter?

Yes. rustfmt by default leaves expressions with too long strings unformatted. Sadly, expressions can be pretty big. In the example below, I added some wrong indentations, and out-of-the-box, rustfmt just leaves things as they are. Observe where I suggest several lines to be added. It could be that the "Lorem Ipsum" string isn’t even on the same screen page as let y.

Let me repeat: only the length of the Lorem Ipsum string is what blocks formatting of let y = and all the other formatting issues.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use some_crate_we_are_dropping::legacy_function;   
use some_upstream_crate::{some_loop, CustomThiserrorEnumType};   
  
#[deprecated]   
pub async fn handle_some_rare_edge_case_for_now_aye() -> Result<(), CustomThiserrorEnumType> {   
    some_high_level_function(|| {   
        match (crate::some_other_mod::modern_function(), legacy_function()) {   
            (None, None) => {   
                let x = some_function().and_then(some_callable);   
                    let y = other_function(); // too far indented   
                    
                match asdf(x, y) {   
                    Ok(g) => g,   
                    Err(e) => Err(CustomThiserrorEnumType::Asdf(e))   
                    } // too far indented                       
                // imagine 30 more lines here   
            }                               
            (Some(x), Some(y)) if x == y => Ok(Some(x)),
            (Some(x), Some(y)) => Err(CustomThiserrorEnumType::OneVariant(
                AAAAAAAAAErrorEnum::OtherError(anyhow::anyhow!(
                    "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lore"
            )), // too little indented      
            )),                             
                    (Some(x), None) | (None, Some(x)) => Ok(Some(x)), // too far indented
        }           
    })              
    .await          
}

Failure modes

Sadly, with the configuration that I suggest above, the formatter is not guaranteed to succeed. Normally, one can just format code without handling failures.

A simple link can be too long to result in unformattable code

1
2
3
// Remember the use case in https://gitlab.com/sudo_piko/regexle/-/blob/main/decoder/script.js?ref_type=heads#L9
fn post_to_regexle() {
…

I haven’t come up with a way to format this in a way that my rustfmt setting can handle. The link just is too long.

let (many, assignments) = call

TODO, I have a line of code in non-public code, which I haven’t managed to turn into a standalone example yet.

far indented long identifier

I’m for other reasons wondering if indentation should better not be counted for column limits (only count columns from first non-space to last non-space). Ignoring the reasons, let’s look at a far indent

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
enum Ka {
    Blu,
    Blo,
}

mod sub_mod {
    impl Ka {
        fn bla(self, x: [[[[i32]]]], other: Ka) {
            if true {
                for a in x {
                    if true {
                        for b in a {
                            if true {
                                for y in b {
                                    if true {
                                        for z in y {
                                            match other {
                                                Blu | Blo => match self {
                                                    Blu | Blo => {
                                                        if true {
                                                            fn inner(e: i32) {
                                                                let _critical_object_with_long_name =
                                                                    long_function_name(e);
                                                            }
                                                            inner(z);
                                                        }
                                                    }
                                                },
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

rustfmt will give up with

error[internal]: line formatted, but exceeded maximum width (maximum: 100 (see `max_width` option), found: 101)
  --> /home/pseyfert/coding/blog/research/rustfmt/example/ind.rs:22:22:101
   |
22 | ...                   let _critical_object_with_long_name =
   |                                                           ^
   |

warning: rustfmt has failed to format. See previous 1 errors.

Again nothing that can be done automatically. You probably should break up the block into a call stack. From the formatting point of view, the code is unformattable.

Conclusion

What do I want:

Not within this post (read: if I ever write those posts, I should link them here):