When the file isn't there
You're building a CLI tool that reads a configuration file. The user runs the command, and the tool crashes because config.toml doesn't exist. In Python, you might catch an exception. In JavaScript, you'd check a callback or handle a promise rejection. In Rust, the compiler stops you from writing code that assumes the file exists. Every file system operation returns a Result. You must handle the success case and the error case before the program compiles. std::fs is the module that provides these operations. It wraps every result in a Result so you can't ignore failures.
The strict form analogy
Think of std::fs functions like a strict bureaucratic form. You submit a request to read a file. The system doesn't just hand you the data. It gives you back a sealed envelope. The envelope is either green (success, containing the data) or red (error, containing a description of what went wrong). You have to open the envelope and check the color before you can use the data.
This is the Result type. Ok(T) is the green envelope containing your value. Err(E) is the red envelope containing an error. The compiler enforces this pattern. You cannot extract the data from an Ok without acknowledging that an Err might exist. This design prevents your program from crashing when a file is missing, the disk is full, or permissions are wrong. It forces you to decide what happens when things go wrong.
The envelope is either green or red. Check the color before you use the data.
Minimal example: reading with match
Here is the standard pattern for reading a file. The code uses match to handle both outcomes.
use std::fs;
use std::path::Path;
/// Reads a file and prints its contents, handling errors explicitly.
fn read_config() {
// Path::new creates a borrowed path reference from a string slice.
// This is the standard way to construct paths for fs functions.
let path = Path::new("config.txt");
// fs::read_to_string returns a Result<String, io::Error>.
// The compiler forces us to handle both the Ok and Err cases.
match fs::read_to_string(path) {
Ok(contents) => {
// We got the data. The file exists and was readable.
// contents is a String owned by this scope.
println!("Config loaded: {}", contents);
}
Err(e) => {
// Something went wrong. The error type gives details.
// We print the error so the user knows what happened.
eprintln!("Failed to read config: {}", e);
}
}
}
You'll see fs::read_to_string("file.txt") in tutorials. That works because &str implements AsRef<Path>. However, when you start manipulating paths or passing them around, use Path or PathBuf. It signals intent and avoids accidental string manipulation on file paths. The community treats Path as the canonical type for path operations.
Handle the error. Don't let the compiler guess for you.
What happens under the hood
When you call fs::read_to_string, Rust translates that into a system call to the operating system. The OS checks the disk, permissions, and file existence. If everything is fine, the OS returns the bytes. Rust wraps those bytes in a String and puts it inside Ok.
If the OS reports an error, Rust creates an io::Error describing the failure and puts it inside Err. The error contains an OS-specific error code and a human-readable message. The compiler guarantees you handle the Err variant. You can't accidentally use the contents of a file that failed to load.
This is why you see expect in quick scripts. expect unwraps the Result and panics if it's an error. That's fine for prototypes where a missing file means your setup is broken. In production code, panics crash the application. Use match, if let, or the ? operator to handle errors gracefully.
Realistic example: writing with the question mark
Real code often chains multiple operations. The ? operator makes this clean. It unwraps the value on success or returns the error immediately if the call fails.
use std::fs;
use std::io;
/// Writes data to a file, creating directories if needed.
/// Returns an io::Error if any step fails.
fn write_backup(data: &str) -> io::Result<()> {
// fs::create_dir_all creates the directory and all parents.
// It succeeds silently if the directory already exists.
// The ? operator propagates errors up the call stack.
fs::create_dir_all("backups")?;
// fs::write opens, writes, and closes the file in one call.
// If this fails, the function returns the error immediately.
fs::write("backups/latest.txt", data)?;
// We reached here, so the write succeeded.
// Ok(()) signals success with no return value.
Ok(())
}
fs::write is a convenience wrapper. It opens the file, writes the data, and closes the file in one call. Use it for simple operations. When you need to write in chunks, append to a file, or keep the file open for multiple writes, reach for std::fs::File instead. The File type gives you control over buffering and file descriptors.
The ? operator keeps your error handling clean. Let errors bubble up until you can handle them.
Inspecting errors
Sometimes you need to react differently based on the error type. io::Error provides methods to inspect the error kind. This lets you distinguish a missing file from a permission denied error.
use std::fs;
use std::io;
/// Attempts to read a file and provides specific feedback for common errors.
fn load_with_feedback(path: &str) {
match fs::read_to_string(path) {
Ok(contents) => println!("Loaded: {}", contents),
Err(e) if e.is_not_found() => {
// The file does not exist.
// We can suggest creating it or using a default.
eprintln!("File not found: {}. Please create it.", path);
}
Err(e) if e.is_permission_denied() => {
// The user lacks read permissions.
eprintln!("Permission denied: {}. Check file permissions.", path);
}
Err(e) => {
// Some other error occurred.
eprintln!("Unexpected error: {}", e);
}
}
}
io::Error provides helper methods like is_not_found() and is_permission_denied(). Use these instead of comparing ErrorKind directly. They are more readable and future-proof. The community prefers the method form because it avoids exposing the enum variant directly.
Respect the Result. It's not a suggestion; it's a contract.
Pitfalls and compiler errors
The compiler rejects code that ignores errors. If you write let content = fs::read_to_string("file.txt");, you get E0308 (mismatched types). The function returns a Result<String, io::Error>, but you're trying to assign it to a variable that expects a String. You must unwrap, match, or use ?.
fs::remove_dir only works on empty directories. If the directory contains files, the call fails with an error. Use fs::remove_dir_all to delete a directory tree. This function is recursive and removes everything inside. Use it carefully.
Never concatenate paths with string formatting. Use path.join("subdir"). It handles the OS-specific separator and avoids double slashes or missing slashes. String concatenation breaks on Windows where separators are backslashes. Path::join works everywhere.
If you try to return a Result from a function that doesn't declare a return type, the compiler complains. Make sure your function signature matches the operations you perform. If you use ?, the function must return a type that implements From<io::Error>, usually io::Result<T> or Result<T, E> where E can convert from io::Error.
Pick the tool that matches the operation. Convenience functions hide complexity; use them when the complexity is low.
Decision matrix
Use fs::read_to_string when you need the entire file content as a String and the file is reasonably small. Use fs::read when you need raw bytes or the file contains binary data. Use fs::write when you want to overwrite a file with new content in a single operation. Use std::fs::File when you need to stream data, append to a file, or keep the file handle open for multiple writes. Use fs::remove_file to delete a single file. Use fs::remove_dir only when you are certain the directory is empty. Use fs::remove_dir_all when you need to delete a directory and all its contents. Reach for std::path::Path when you are passing paths between functions or manipulating directory structures. Reach for std::path::PathBuf when you need to store a path in a struct or return a path from a function.