Standard Input and Output

Standard Output

Standard output (stdout, file descriptor 1 on Unix and STD_OUTPUT_HANDLE on Windows) is the primary output stream used by a process to emit normal program results, structured data (JSON, CSV, TSV), and user-facing text. Every call to print! and println! writes to std::io::stdout().

Buffering and Synchronization of Stdout

Like Stdin, std::io::Stdout is synchronized across threads by a global re-entrant lock and wrapped in a LineWriter by default:

  • Line-buffered: Writing a newline byte (\n, as println! always does) automatically flushes the internal buffer to the operating system.
  • Partial lines (print!): Calling print!("Enter name: ") without a trailing \n keeps the bytes inside the LineWriter buffer until either a \n is written, the buffer fills up, or io::stdout().flush() is called explicitly!
`print!` / `println!` -> `io::stdout()` -> Global `ReentrantLock<LineWriter<RawStdout>>`
                                                 |
     +-------------------------------------------+-------------------------------------------+
     | Contains `\n` (`println!`)                                                            | No `\n` (`print!`)
     v                                                                                       v
  Flushes immediately to OS fd 1                                            Stays in RAM buffer until
                                                                            `io::stdout().flush()` is called!

Explicit Locking (stdout().lock()) for High-Throughput Output

Every standalone println!(...) call acquires and releases the global stdout lock. When printing thousands of lines in a loop, calling io::stdout().lock() (and wrapping it in a BufWriter) acquires the lock once and batches writes into 8 KiB chunks instead of flushing on every \n:

use std::io::{self, BufWriter, Write};

fn main() -> io::Result<()> {
    let stdout = io::stdout();
    // Lock `stdout` once and wrap `StdoutLock` in a `BufWriter` to avoid
    // per-line lock traffic and per-line `\n` syscalls:
    let mut handle = BufWriter::new(stdout.lock());

    for id in 1..=3 {
        writeln!(handle, "record_id={id} status=ok")?;
    }

    // Flush the `BufWriter` before dropping:
    handle.flush()?;
    Ok(())
}
record_id=1 status=ok
record_id=2 status=ok
record_id=3 status=ok

Broken Pipe Handling (writeln! vs. println!)

When a Rust binary's standard output is piped into a command that exits early (such as my_app | head -n 5), the OS closes the pipe and subsequent writes to stdout return Err(io::ErrorKind::BrokenPipe) (EPIPE):

  • println!(...) panics if writing to stdout fails!
  • writeln!(io::stdout(), ...) returns io::Result<()> so CLI tools can catch ErrorKind::BrokenPipe and exit cleanly with code 0 instead of printing a panic message.