#187 Jun 7, 2026

187. fmt::Write — Stop Allocating a Temp String Just to Append It

out.push_str(&format!("{name}: {score}")) builds a brand-new String, copies it into out, then throws it away — every single iteration. One use std::fmt::Write; and write! formats straight into your buffer instead.

The double-allocation habit

This pattern is everywhere, and it allocates a temporary String per call just to immediately copy and drop it:

1
2
3
4
5
6
7
let scores = [("ferris", 100), ("hermit", 42)];

let mut out = String::new();
for (name, score) in scores {
    out.push_str(&format!("{name}: {score}\n")); // temp String, copy, drop
}
assert_eq!(out, "ferris: 100\nhermit: 42\n");

Clippy even has a lint for it: format_push_string.

write! into the String directly

String implements std::fmt::Write, so the same write!/writeln! macros you use in Display impls work on it. The formatted output lands directly in the existing buffer — no intermediate allocation:

1
2
3
4
5
6
7
8
9
use std::fmt::Write; // bring the trait into scope

let scores = [("ferris", 100), ("hermit", 42)];

let mut out = String::new();
for (name, score) in scores {
    writeln!(out, "{name}: {score}").unwrap();
}
assert_eq!(out, "ferris: 100\nhermit: 42\n");

The .unwrap() looks scary but isn’t: write! returns fmt::Result because the trait allows failure, yet writing into a String can never fail — it just grows. let _ = writeln!(...) works too if you prefer.

Why it matters

The format! version allocates N temporary strings for N iterations. The write! version allocates only when out needs to grow — amortized, that’s a handful of reallocations total. In hot loops building large strings (reports, codegen, SQL), the difference shows up in profiles.

One gotcha: std::fmt::Write is for UTF-8 sinks (String); std::io::Write is for byte sinks (files, stdout). Same macro, different trait — if write!(out, ...) complains about no method named write_fmt, you imported the wrong one.

← Previous 186. str::split_inclusive — Split a String and Keep the Separator With Each Chunk Next → 188. LazyLock::from — Skip the Closure When You Already Have the Value