---
title: How to Use stdin and stdout in Rust
summary: Read user input with io::stdin().read_line() and display output with println! in Rust.
lang: en
slug: how-to-use-stdin-and-stdout-in-rust
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-use-stdin-and-stdout-in-rust/
---

# How to Use stdin and stdout in Rust



## The input that never arrives

You're building a command-line tool. You print a prompt asking the user for a number, wait for input, and read the result. It works perfectly the first time. You wrap it in a loop to ask again, and suddenly the program skips the second prompt and crashes. Or you print a message, but the text doesn't appear on the screen until the program exits. The terminal feels broken. It isn't. Rust's I/O model is explicit about buffering, ownership, and error handling. Ignoring those details turns a simple prompt into a debugging session.

## Streams, buffers, and the hose analogy

Standard input and standard output are streams of bytes. Think of `stdin` as a hose delivering water from the keyboard. Think of `stdout` as a hose carrying water to the screen. Rust doesn't assume you want to drink from the hose every millisecond. It gives you a bucket and lets you decide when to fill it.

The `std::io` module manages these hoses. `stdin()` hands you the handle to the input hose. `stdout()` hands you the handle to the output hose. The key difference from higher-level languages is that Rust makes the buffering visible. The screen isn't updated instantly. Data sits in a buffer until it's full or you force a flush. The keyboard input isn't available character-by-character; it arrives in chunks, usually line-by-line. Rust forces you to acknowledge these mechanics through the type system and error handling.

## Reading a single line

The most common task is reading a line of text. `std::io::stdin()` returns a `Stdin` struct. You call `read_line` on it, passing a mutable string to fill.

```rust
use std::io;

fn main() {
    // Create a mutable String to hold the input.
    // read_line appends to the string, so it must be mutable.
    let mut buffer = String::new();

    // stdin() returns a Stdin handle.
    // read_line reads until a newline and appends to buffer.
    io::stdin()
        .read_line(&mut buffer)
        .expect("Failed to read line");

    // Print what we got.
    println!("You typed: {buffer}");
}
```

`read_line` blocks the program. The CPU waits until the user presses Enter. The function reads bytes, including the newline character `\n`, and appends them to the provided string. It returns a `Result<usize, io::Error>`. The `usize` is the number of bytes read. The `Result` forces you to handle errors, like a disconnected pipe or a permission issue. `expect` panics if there's an error, which is acceptable for a quick script but not for production code.

Don't forget the newline. `read_line` keeps the `\n` at the end. If you compare the input to a string, the comparison fails. If you parse the input as a number, the parser fails. You almost always need to trim the result.

## What happens under the hood

When you call `io::stdin()`, Rust creates a `Stdin` object. This is a handle, not the data itself. Calling `read_line` on the handle acquires an internal lock, reads from the operating system's file descriptor, and releases the lock. The read operation blocks until data is available.

The function appends to the string. It does not replace the string. If you call `read_line` multiple times on the same string, the new input is added to the end of the old input. This is a common source of bugs in loops. You must clear the string between reads, or create a new string each time.

The return value is a `Result`. If the read succeeds, you get `Ok(n)` where `n` is the byte count. If the input stream ends, you get `Ok(0)`. If an error occurs, you get `Err(e)`. Rust doesn't hide these cases. You have to decide what to do with each one.

## The realistic loop

A real tool usually loops, parses input, and handles errors gracefully. This example reads numbers until the user types "quit". It demonstrates trimming, parsing, and error handling.

```rust
use std::io;

fn main() {
    println!("Enter numbers. Type 'quit' to stop.");

    loop {
        // Create a fresh string for each iteration.
        // Reusing a string without clearing it causes data to accumulate.
        let mut input = String::new();

        // Read the line. Expect panics on error, which is fine for this demo.
        io::stdin().read_line(&mut input).expect("Read failed");

        // Trim removes the trailing newline and whitespace.
        // Without trim, "123\n" fails to parse as an integer.
        let text = input.trim();

        if text == "quit" {
            break;
        }

        // Parse the string into an i32.
        // parse returns a Result, so we handle the error case.
        match text.parse::<i32>() {
            Ok(n) => println!("Got number: {n}"),
            Err(_) => println!("'{text}' is not a valid number."),
        }
    }
}
```

The `trim()` call is essential. It strips the newline and any surrounding spaces. The `parse::<i32>()` call attempts to convert the string to an integer. It returns `Ok(n)` on success or `Err(e)` on failure. The `match` expression handles both cases without panicking.

Convention aside: `trim()` is the standard way to clean up `read_line` output. The community expects you to trim before parsing or comparing. Leaving the newline in place is considered a bug.

## Pitfalls that trip up beginners

I/O in Rust has a few sharp edges. The compiler catches some, but others only show up at runtime.

### The invisible newline

`read_line` includes the newline character in the result. If you write `if input == "yes"`, the condition is false because the input is `"yes\n"`. The compiler won't warn you. The program just behaves wrong. Always trim.

### The buffering trap

`stdout` is buffered. When you print text, it goes into a buffer first. The buffer is flushed to the screen when it fills up, when the program exits, or when you call `flush()`. `println!` adds a newline and flushes automatically. `write!` does not.

If you use `write!` to print a prompt and then call `read_line`, the prompt might not appear. The user sees a blank screen and types blindly. The prompt appears only after the user presses Enter. You must flush the output before reading.

```rust
use std::io::{self, Write};

fn main() {
    let mut stdout = io::stdout();

    // write! does not flush. The prompt stays in the buffer.
    write!(stdout, "Name: ").expect("Write failed");

    // Flush forces the buffer to the screen.
    stdout.flush().expect("Flush failed");

    let mut name = String::new();
    io::stdin().read_line(&mut name).expect("Read failed");
    println!("Hello, {}", name.trim());
}
```

If you forget the flush, the prompt is delayed. Don't fight the buffer. Flush it.

### EOF is silent

When the input stream ends, `read_line` returns `Ok(0)`. It does not return an error. The string is not cleared. If you loop without checking the byte count, you process empty strings forever.

```rust
use std::io;

fn main() {
    let mut buffer = String::new();

    loop {
        // Clear the buffer before reading.
        buffer.clear();

        match io::stdin().read_line(&mut buffer) {
            Ok(0) => {
                // 0 bytes means EOF. Break the loop.
                break;
            }
            Ok(_) => {
                println!("Got: {}", buffer.trim());
            }
            Err(e) => {
                eprintln!("Error: {e}");
                break;
            }
        }
    }
}
```

Check the return value. EOF is silent. If you ignore it, your loop never ends.

### Standard error

`println!` writes to standard output. Errors should go to standard error. Use `eprintln!` for error messages. Stderr is usually unbuffered, so error messages appear immediately even if stdout is buffered. This keeps error output separate from program output, which matters when piping data.

```rust
use std::io;

fn main() {
    let mut input = String::new();
    match io::stdin().read_line(&mut input) {
        Ok(_) => println!("Success: {}", input.trim()),
        Err(e) => eprintln!("Read error: {e}"),
    }
}
```

Send errors to stderr. It's the convention, and it prevents error messages from polluting data streams.

## Performance and conventions

Calling `stdin()` repeatedly in a loop creates a new handle each time. The handle is cheap, but it involves locking overhead. For tight loops, use `stdin().lock()`. The lock returns a `StdinLock` that holds the lock for the duration of the borrow. This avoids repeated locking and unlocking.

```rust
use std::io::{self, BufRead};

fn main() {
    let stdin = io::stdin();
    // lock() returns a StdinLock that implements BufRead.
    // This is efficient for repeated reads.
    let handle = stdin.lock();

    // lines() returns an iterator over Result<String>.
    // It handles the loop and trimming for you.
    for line in handle.lines() {
        match line {
            Ok(l) => println!("Line: {l}"),
            Err(e) => eprintln!("Error: {e}"),
        }
    }
}
```

`BufRead` is a trait that provides buffered reading methods. `stdin().lock()` implements `BufRead`. The `lines()` method returns an iterator that yields `Result<String>` for each line. It handles the loop, the trimming, and the EOF check. This is the idiomatic way to process all input.

Convention aside: Use `stdin().lock()` in loops. The community calls this the "lock once" pattern. It's a small optimization that signals you understand I/O mechanics.

## Decision matrix

Use `read_line` when you need to read a single line of text into a `String` and handle the result manually. Use `lines()` when you want to iterate over all input until EOF with automatic trimming and error handling. Use `read` when you need raw bytes or fixed-size chunks without line processing. Use `write!` when you need to output text without a newline. Use `println!` for simple output with automatic flushing. Use `stdout().flush()` when you print a prompt and need it visible before reading. Use `eprintln!` for error messages to keep them separate from program output. Use `stdin().lock()` when reading in a loop to avoid locking overhead.

Trust the `Result`. Panicking on I/O is a recipe for crashes. Handle the errors, check the byte count, and trim your strings.

## Where to go next

- [How to Handle File Permissions in Rust](/en/how-to-handle-file-permissions-in-rust/)
- [How to Memory-Map Files in Rust (memmap)](/en/how-to-memory-map-files-in-rust-memmap/)
- [How to use stdin and stdout](/en/how-to-use-stdin-and-stdout/)