Standard Input and Output

Handling Input Errors

Input operations in Rust can fail at two distinct layers:

  1. The I/O transport layer (std::io::Error): OS read failures, broken pipes, premature End-of-File (UnexpectedEof), or non-UTF-8 byte sequences (InvalidData).
  2. The domain parsing layer (ParseIntError, ParseFloatError, custom validation): The UTF-8 text was read from the stream, but its contents do not match the expected grammar or numeric range.

Key std::io::ErrorKind Variants Encountered on Input

io::ErrorKind Variant When It Occurs on Input Recommended Handling
InvalidData read_line, lines, or read_to_string encountered bytes that are not valid UTF-8 Switch to byte-oriented .read_until(b'\n', &mut vec) + String::from_utf8_lossy if arbitrary bytes are expected
UnexpectedEof read_exact(&mut buf) reached EOF before filling buf completely Report truncated file/frame or treat 0 bytes read on frame boundary as clean shutdown
Interrupted (EINTR) A POSIX signal interrupted a blocking syscall before bytes were read Automatically retried by read_exact, read_line, read_to_end; retry manually in raw .read() loops
WouldBlock (EAGAIN) Nonblocking stream has no data ready right now Register wakeup or poll again later
PermissionDenied / NotFound Opening or reading a restricted or missing file Surface path and OS error message to user

Handling Invalid UTF-8 Input Without Failing (read_until + from_utf8_lossy)

Because BufRead::read_line(&mut String) and BufRead::lines() require valid UTF-8, a single non-UTF-8 byte (0xFF) in a log file or terminal stream causes read_line to return Err(io::ErrorKind::InvalidData).

When reading log files or streams that might contain arbitrary binary or Latin-1 bytes alongside ASCII text, read raw bytes into a Vec<u8> using reader.read_until(b'\n', &mut byte_buf) and convert with String::from_utf8_lossy(&byte_buf) (which borrows valid UTF-8 as Cow::Borrowed(&str) at zero cost and replaces invalid bytes with ``):

use std::io::{self, BufRead, Cursor};

fn main() -> io::Result<()> {
    // Stream containing an invalid UTF-8 byte `0xFF` on line 1:
    let non_utf8_stream: &[u8] = b"valid prefix \xFF invalid byte\nsecond clean line\n";

    // 1. `read_line` into a `String` rejects invalid UTF-8 with `ErrorKind::InvalidData`:
    let mut str_buf = String::new();
    let utf8_err = Cursor::new(non_utf8_stream)
        .read_line(&mut str_buf)
        .unwrap_err();
    println!("read_line error kind on 0xFF: {:?}", utf8_err.kind());

    // 2. `read_until(b'\n', &mut byte_buf)` + `from_utf8_lossy` handles non-UTF-8 gracefully:
    let mut reader = Cursor::new(non_utf8_stream);
    let mut byte_buf = Vec::new();
    while reader.read_until(b'\n', &mut byte_buf)? > 0 {
        let lossy = String::from_utf8_lossy(&byte_buf);
        println!("lossy line: {:?}", lossy.trim_end());
        byte_buf.clear();
    }

    Ok(())
}
read_line error kind on 0xFF: InvalidData
lossy line: "valid prefix \u{fffd} invalid byte"
lossy line: "second clean line"

Validated Retry Loop for Interactive User Input

When prompting a user for typed input (such as an integer within a range 1..=100), separate stream EOF / I/O errors (which should exit the loop) from parse / validation errors (which should print a helpful message and prompt again):

use std::io::{self, BufRead, Cursor};

fn read_validated_port(mut reader: impl BufRead) -> io::Result<Option<u16>> {
    let mut buf = String::new();

    loop {
        buf.clear();
        if reader.read_line(&mut buf)? == 0 {
            // Clean EOF reached without a valid port:
            return Ok(None);
        }

        let trimmed = buf.trim();
        match trimmed.parse::<u16>() {
            Ok(port) if port >= 1024 => return Ok(Some(port)),
            Ok(port) => println!("port {port} is privileged (< 1024); try again"),
            Err(err) => println!("invalid integer {:?} ({err}); try again", trimmed),
        }
    }
}

fn main() -> io::Result<()> {
    // User types "abc", then "80" (privileged), then "8080" (valid):
    let simulated_user = Cursor::new("abc\n80\n8080\n");
    let chosen_port = read_validated_port(simulated_user)?;

    println!("accepted port: {:?}", chosen_port);
    Ok(())
}
invalid integer "abc" (invalid digit found in string); try again
port 80 is privileged (< 1024); try again
accepted port: Some(8080)