A guessing game's first hurdle
You're following along with the Rust book and you've reached the guessing-game chapter. The program is supposed to ask the user for a number and tell them if they guessed too high or too low. The very first thing it has to do is read what they typed. In Python that's input("guess: "). In JavaScript with Node it's readline. In Rust... well, it's a few lines, and they look strange the first time:
use std::io;
fn main() {
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
Why a mutable String? Why &mut in the call? What does .expect() do? The shape of this snippet is doing a lot of work, and once you understand each piece, reading from stdin in any program becomes second nature.
What io::stdin() actually returns
std::io::stdin() returns a handle to your program's standard input stream. The handle is small and cheap to create. Internally, it's a wrapper around the file descriptor that the OS gave your process at startup (descriptor 0, traditionally). The handle is the door; reading is what you do at the door.
The handle has several methods. The two most common are:
read_line(&mut buf): reads up to and including a newline.read_to_string(&mut buf): reads all input until EOF (Ctrl-D on Unix, Ctrl-Z then Enter on Windows).
read_line is the one you want for "ask the user one question and wait for them to press Enter."
Why the buffer is mutable
Look at read_line(&mut guess). We pass a mutable reference to a String we created. Why? Because Rust's standard input API is caller-allocated: you bring the buffer, the function fills it. There's no return-a-new-String version of read_line in the basic API.
This isn't an accident. Reading is a streaming activity; you often do it in a loop, appending each line to the same buffer, or to different buffers depending on what you've parsed. Forcing the caller to provide the buffer lets you control allocation. If you wanted, you could String::with_capacity(64) to pre-allocate, or reuse a single buffer across many calls.
The downside is the snippet looks bulkier than let guess = input(); would. The trade-off Rust made is "explicit and reusable" over "compact for one-line scripts."
use std::io;
fn main() {
// The buffer must be `mut` because read_line will append into it.
let mut name = String::new();
println!("What's your name?");
// Pass it by mutable reference. Rust's borrow checker enforces that
// nothing else is reading the same String while this function writes.
io::stdin().read_line(&mut name).expect("read failed");
// read_line keeps the trailing newline. trim_end strips it (and any \r on Windows).
let name = name.trim_end();
println!("Hello, {}!", name);
}
The trim_end() step is important. read_line includes the user's \n in the buffer, because it can't know whether you wanted it or not. If you println!("Hello, {}", name) without trimming, you'll see an unexpected blank line in your output.
Returning Result instead of expecting
expect("Failed to read line") is fine for tutorials and prototype code. It panics on any error, which kills your program. For real programs, propagate the error properly.
use std::io;
// Returning io::Result lets the caller decide what to do on failure.
fn ask_name() -> io::Result<String> {
let mut name = String::new();
// The `?` operator returns the error early if read_line fails.
io::stdin().read_line(&mut name)?;
Ok(name.trim_end().to_string())
}
fn main() -> io::Result<()> {
let name = ask_name()?;
println!("Hello, {}!", name);
Ok(())
}
main is allowed to return io::Result<()>. If ? propagates an error all the way up, the program prints a Debug version of the error and exits with a non-zero status. That's almost always what you want for a real CLI.
Reading many lines
If you're reading a longer stream (a file piped in, several lines at a time), construct a BufReader and use the lines() iterator. This avoids one syscall per line and gives you nice iterator ergonomics.
use std::io::{self, BufRead};
fn main() -> io::Result<()> {
// Lock stdin once, wrap it in a BufReader. The lock is held for the rest
// of main; nothing else in this thread can read stdin in parallel.
let stdin = io::stdin();
let reader = stdin.lock();
let mut total = 0;
for line in reader.lines() {
// Each `line` is io::Result<String>. ? unwraps or propagates the error.
let line = line?;
total += line.len();
}
println!("read {} characters total", total);
Ok(())
}
A few things to notice. stdin().lock() is the canonical pattern: it gives you a guarantee that nothing else is grabbing stdin in this thread, and it returns a type that implements BufRead, which has the lines() method. The locked handle is what enables efficient line-by-line reading.
Each item from lines() is an io::Result<String>. The string already has the trailing newline stripped, which is convenient.
Parsing what you read
Reading is just step one. Most of the time you actually want a number, a date, a command. The standard pattern is parse():
use std::io;
fn main() -> io::Result<()> {
let mut input = String::new();
io::stdin().read_line(&mut input)?;
// parse() returns Result<T, T::Err>. We turn the error into io::Error
// so we can use ? in a function that returns io::Result.
let number: i32 = input
.trim()
.parse()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
println!("you typed: {}", number * 2);
Ok(())
}
parse() is generic. The : i32 annotation tells it which target type to produce. You can parse into f64, bool, IpAddr, anything that implements the FromStr trait.
If the user types "abc" when you expected a number, parse returns an error and ? propagates it. The program exits with a useful message instead of crashing on unwrap. For interactive programs, you'd usually catch the error and re-prompt instead.
Common pitfalls
You forgot to trim and your code looks broken:
Hello, alice
!
That's the trailing \n showing up. Add .trim() or .trim_end() after reading.
You wrote read_line(guess) instead of read_line(&mut guess). The compiler tells you:
error[E0308]: mismatched types
expected `&mut String`, found `String`
Add the &mut. The function needs a mutable reference, not the value itself.
You called read_line on a String that wasn't mut:
error[E0596]: cannot borrow `guess` as mutable, as it is not declared as mutable
Add mut in the let binding.
You forgot to handle EOF. On a piped input, read_line returns Ok(0) when there's nothing left. Your loop should check for that, or use lines() which handles it for you.
You used read_to_string and wondered why the program hung. read_to_string waits for EOF. From a terminal, you have to send EOF manually (Ctrl-D on Unix, Ctrl-Z on Windows). read_line reads until Enter, which is usually what you want for interactive prompts.
When to use what
For a single line from a user, read_line(&mut buf) and trim. Simple, idiomatic.
For many lines (piped input, line-by-line processing), lock stdin and iterate with lines(). Faster and cleaner.
For "give me everything that comes in," read_to_string(&mut buf). Fine for small inputs; allocates the whole input as one String, so don't do this with gigabyte streams.
For real interactive prompts (questions with defaults, multi-choice menus, password input), reach for the dialoguer crate. It handles redraws, masking, and arrow-key navigation that you'd otherwise have to write yourself. See How to Read User Input Interactively in Rust (dialoguer).
For full-screen TUIs, crossterm and ratatui.
Where to go next
How to Read User Input Interactively in Rust (dialoguer)