The prompt disappears
You write a Rust program that asks for a name. You run it. The cursor blinks. You type "Alice" and hit Enter. The program prints "Hello, Alice!" and exits. It works.
Now you add a prompt. You change the code to print "Enter your name: " before reading. You run it again. You type "Alice". Nothing happens. You hit Enter. The program prints "Enter your name: Hello, Alice!" all on one line and exits. The prompt appeared too late. The user is confused. The terminal is buffering your output.
Rust batches writes to the screen for performance. It fills a bucket before dumping it. When you write a prompt, the text sits in the bucket. The user types an answer, but never sees the question. You have to flush the buffer manually to force the text onto the screen.
How stdin and stdout work
The terminal connects to your program through two main pipes. Standard input (stdin) carries keystrokes from the keyboard into your program. Standard output (stdout) carries text from your program to the screen.
Rust's standard library provides std::io::stdin() and std::io::stdout(). These return handles to the pipes. You don't read or write characters one by one. That would be too slow. You read into a buffer and write from a buffer.
read_line() reads from stdin until it finds a newline character. It appends the text, including the newline, to a String. The function returns a Result because I/O can fail. The disk might be full. The terminal might close. The user might press Ctrl+D to send an EOF signal.
print! writes to stdout. It does not flush the buffer automatically. println! does flush, but only after it adds its own newline. If you use print! for a prompt, you must flush manually.
Minimal example
This pattern covers the basics: allocate a buffer, print a prompt, flush, read, trim, and handle errors.
use std::io::{self, Write};
/// Reads a name from stdin and greets the user.
fn main() -> io::Result<()> {
let mut input = String::new();
print!("Enter your name: ");
// Flush stdout so the prompt appears immediately
io::stdout().flush()?;
io::stdin().read_line(&mut input)?;
// read_line keeps the newline, so trim it off
let name = input.trim();
println!("Hello, {}!", name);
Ok(())
}
Flush the buffer. The user won't wait for the bucket to fill.
Walkthrough
String::new() allocates an empty buffer on the heap. The string starts with zero capacity. print! writes the prompt to the stdout buffer. The text stays in memory.
io::stdout().flush()? forces the buffer to the terminal. The ? operator propagates errors. If the terminal is closed, flush returns an error and main exits early. This is idiomatic for CLI tools. Returning io::Result<()> from main lets you use ? without wrapping everything in match.
io::stdin().read_line(&mut input)? blocks execution. The OS waits for the user to press Enter. When Enter is pressed, the OS delivers the text plus the newline character to Rust. read_line appends this to the string. It grows the vector as needed. The function returns the number of bytes read. The ? operator discards the count and propagates errors.
input.trim() removes whitespace from both ends. This includes the newline character. On Windows, the newline is \r\n. trim() handles both. The result is a &str slice. println! takes the slice and prints it. println! adds a newline at the end, so the output looks clean.
Convention aside: io::stdout().flush()? is the standard pattern. You will see this in every interactive Rust program. Some developers create a helper function to avoid repetition. The community calls this the "prompt helper" pattern. It keeps main clean.
Realistic example
Real tools validate input. They loop until the user provides something useful. They handle edge cases like empty input and EOF signals.
use std::io::{self, Write};
/// Prompts for a non-empty name until valid input is provided.
fn get_name() -> io::Result<String> {
loop {
print!("Enter name (or 'quit' to exit): ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
// Check for EOF to prevent infinite loops when input is piped
if input.is_empty() {
return Ok(String::new());
}
let trimmed = input.trim();
if trimmed.is_empty() {
println!("Name cannot be empty. Try again.");
continue;
}
if trimmed == "quit" {
return Ok("quit".to_string());
}
return Ok(trimmed.to_string());
}
}
fn main() -> io::Result<()> {
match get_name()? {
"quit" => println!("Goodbye!"),
name => println!("Welcome, {}!", name),
}
Ok(())
}
Validate early. Reject bad input before it reaches your business logic.
Pitfalls and compiler errors
Forgetting to trim the input breaks comparisons. read_line keeps the newline. "Alice\n" is not equal to "Alice". Your validation logic fails silently. The compiler cannot warn you about this. You must trim every time.
Forgetting to flush delays the prompt. The user types blindly. This is a runtime bug, not a compile error. The code runs, but the UX is broken. Test interactive code by running the binary directly. cargo run can sometimes swallow signals or behave differently with buffering depending on the environment.
If you try to print the Result directly, the compiler rejects you with E0277 (trait bound not satisfied). Result does not implement the Display trait. You must unwrap or handle the error first. Use ? to propagate, or match to handle.
Piped input breaks interactive assumptions. When you run echo "Alice" | ./my_program, stdin is a pipe. read_line returns immediately. The loop might spin forever if you don't check for EOF. read_line returns Ok(0) when the pipe closes. The string remains empty. Check input.is_empty() after reading to detect EOF and break the loop.
Convention aside: trim() removes leading whitespace too. If the user types " Alice", trim() makes it "Alice". This is usually desired. If you need to preserve leading spaces, use trim_end() instead. Most CLI tools strip leading whitespace. Stick with trim() unless you have a reason not to.
Don't print the Result. Handle the error or the compiler will stop you.
When to use what
Use std::io::stdin().read_line() when you need a simple, dependency-free way to read a line of text and can handle the boilerplate of flushing and trimming yourself.
Use the dialoguer crate when you are building an interactive CLI that needs validation, password masking, or selection menus without writing custom terminal control logic.
Use the clap crate when your tool requires command-line arguments, flags, and subcommands rather than interactive prompts.
Use std::io::Read::read() when you need to read raw bytes or handle binary data instead of text lines.
Reach for crates when the boilerplate outweighs the benefit. Standard library is fine for scripts; dialoguer is better for tools.