I/O Operations in Rust
Learn console input, formatted output, and file reading and writing in Rust with Result-based error handling and Docker-ready examples
Input and output is where Rust’s philosophy becomes impossible to ignore: I/O can fail, and Rust refuses to let you pretend otherwise. Where C returns error codes you can silently discard and Python raises exceptions you can forget to catch, Rust wraps every fallible I/O operation in a Result that you must explicitly handle—or explicitly propagate with the ? operator.
As a systems language, Rust gives you fine-grained control over the machinery: standard output is line-buffered, so print! without a newline may not appear until you flush; file reads can be buffered through BufReader to avoid a system call per line; and errors carry the underlying OS error code. At the same time, Rust’s functional heritage shows through in how I/O composes—iterating over lines of a file uses the same iterator machinery as iterating over a vector.
In this tutorial you’ll learn formatted output beyond the basic println! you saw in Hello World, how to read from standard input, and how to write, append to, and read files—including what to do when a file doesn’t exist. Along the way you’ll meet the ? operator, the idiomatic way to propagate I/O errors up the call stack.
Formatted Output
println! can do far more than print a static string. Rust’s formatting mini-language supports positional and named arguments, precision, padding, number bases, and a separate “debug” representation for compound types. There is also print! (no trailing newline) and eprintln! (writes to standard error instead of standard output).
Create a file named formatted_output.rs:
| |
The distinction between {} (the Display trait, for user-facing text) and {:?} (the Debug trait, for developer-facing inspection) runs throughout Rust: your own structs get {:?} for free with #[derive(Debug)], but must implement Display by hand.
Reading from Standard Input
Rust reads standard input through std::io::stdin(). The read_line method appends into a String you provide—including the trailing newline, which is why nearly every input line gets .trim() called on it. Parsing text into a number returns another Result, so bad input is caught explicitly rather than crashing mysteriously later.
Create a file named read_input.rs:
| |
Note the ownership at work: read_line borrows the String mutably (&mut name) and appends to it, avoiding an allocation per call if you reuse the buffer. The expect calls convert an error into a controlled crash with a message—fine for small programs; larger ones propagate errors instead, as the next example shows.
File I/O and Error Handling
For files, std::fs offers one-call conveniences (fs::write, fs::read_to_string) alongside lower-level handles (File, OpenOptions) for appending or streaming. This example writes a file, appends to it, reads it back two different ways, then deliberately tries to read a file that doesn’t exist.
Notice the return type of main: because it returns std::io::Result<()>, every fallible call can end with ?, which means “if this failed, return the error from main right now; otherwise unwrap the value.”
Create a file named file_io.rs:
| |
The final match shows the alternative to ?: pattern-matching on the Result lets the program recover from a missing file rather than exit. The error’s Display output includes the underlying OS error, so diagnostics come for free.
Running with Docker
| |
The stdin example needs Docker’s -i flag so the piped input reaches the program; without it, read_line sees an immediate end-of-file.
Expected Output
Running formatted_output (the stderr line appears interleaved in a terminal):
Loading...done
input and output; output and input
Rust first appeared in 2010
pi to 2 places: 3.14
padded: 42
hex: ff, binary: 101
debug: (3, 7)
this goes to standard error
Running read_input with the piped input Ada, 2, 40:
What is your name?
Hello, Ada! Enter two numbers, one per line.
Their sum is 42
Running file_io:
--- journal.txt (79 bytes) ---
1: day 1: learned println!
2: day 2: learned ownership
3: day 3: learned the ? operator
could not read missing.txt: No such file or directory (os error 2)
Key Concepts
- I/O failures are values, not exceptions — every fallible operation returns
io::Result, and the compiler warns if you ignore one - The
?operator propagates errors — it unwraps anOkor returns theErrfrom the current function, which is whymaincan be declared asfn main() -> std::io::Result<()> read_linekeeps the newline — call.trim()before comparing or parsing input, and remember it appends to the buffer you pass in{}vs{:?}—Displayformatting is for users,Debugformatting is for developers; deriveDebugon your own types to make them printable- stdout is line-buffered —
print!without a newline may not appear until you callio::stdout().flush() eprintln!writes to stderr — keep diagnostics out of stdout so your program’s real output can be piped cleanly- Choose the right file API —
fs::write/fs::read_to_stringfor one-shot convenience,FileplusBufReaderfor streaming large files line by line,OpenOptionsfor appending - Recover with
match, propagate with?— pattern-match aResultwhen the program can continue (e.g., a missing optional file), use?when the caller should decide
Comments
Loading comments...
Leave a Comment