write! / writeln!Macro
Explanation
write!/writeln! use the exact same format-string grammar as
format!, but instead of allocating and returning a
fresh String, they write the formatted text into a destination the
caller already has — the first argument to either macro is that
destination, followed by the format string and its arguments:
write!(destination, "...", args...). writeln! additionally appends a
trailing newline, the same relationship println! has to print!.
The destination can implement either of two different, similarly-named
but distinct Write traits, and which one it is changes what "writing"
actually means: std::fmt::Write (also available as core::fmt::Write,
needing no allocator) is implemented by String, and it's what a custom
Display
impl's fmt method writes into via its &mut Formatter argument;
std::io::Write is implemented by files, TCP sockets, and locked
standard output/error, and represents writing bytes somewhere an actual
I/O operation might fail.
That difference shows up directly in the return type: both macros return
a Result, but for an fmt::Write target the error case (fmt::Error)
is essentially unreachable in ordinary code — the operation only fails if
the underlying Write impl itself reports failure, which String's
never does — so it's common, and generally fine, to .unwrap() it. For
an io::Write target, though, the Result represents a real I/O
failure (a full disk, a broken pipe, a closed socket), and production
code should propagate it with ? rather than unwrap, exactly the way any
other fallible I/O call is handled.
Usage examples
Writing formatted text into a String
use std::fmt::Write as _;
fn build_summary(item_count: u32, total: f64) -> String {
let mut summary = String::new();
write!(summary, "{item_count} items, ${total:.2} total").unwrap(); // <- fmt::Write for String never actually fails
summary
}
let summary = build_summary(3, 47.5);
Implementing Display by writing into a Formatter
A custom Display impl for a duration-like type formats its value piece
by piece using write!, avoiding an intermediate allocation just to
combine the pieces.
use std::fmt;
struct Elapsed { seconds: u64 }
impl fmt::Display for Elapsed {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let minutes = self.seconds / 60;
let remaining = self.seconds % 60;
write!(f, "{minutes}m {remaining:02}s") // <- writes straight into the Formatter, no intermediate String
}
}
let elapsed = Elapsed { seconds: 125 };
println!("{elapsed}"); // 2m 05s
The
std fmt docs
show writing directly into the &mut Formatter that Display::fmt is
handed as the idiomatic implementation shape; building an intermediate
String with format! just to immediately write it out again is an
unnecessary allocation on every call.
Appending log lines to a file
Appending log lines to an open file propagates the io::Write Result
with ? rather than unwrapping it, since the destination is a real OS
resource whose writes can genuinely fail.
use std::fs::File;
use std::io::{self, Write};
fn log_reading(file: &mut File, sensor_id: u32, value: f64) -> io::Result<()> {
writeln!(file, "sensor {sensor_id}: {value:.2}")?; // <- io::Write target: a real I/O error must be propagated, not unwrapped
Ok(())
}
An io::Write destination can genuinely fail (disk
full, broken pipe), so production code propagates the Result with ?
the same way any other fallible I/O call is handled — the
Book's error-handling chapter
treats unwrapping a fallible I/O result as acceptable only in throwaway
code, unlike the fmt::Write-into-a-String case above where
.unwrap() is standard practice.