The toolbox that ships with Rust
You run cargo new, open main.rs, and stare at an empty fn main(). You want to read a file, split a string, or spin up a background task. You don't need to hunt for crates. You don't need to run cargo add. The tools are already there, waiting in the standard library.
Rust ships with a standard library that feels like a well-organized Swiss Army knife: heavy enough for systems work, sharp enough for quick scripts, and designed so you never accidentally cut yourself. It provides the core types you use every day, the collections that manage memory for you, and the I/O primitives that talk to the operating system. Everything is accessible via std::, and the most common types are available immediately through the prelude.
What the standard library actually is
The standard library lives in the std crate. Every Rust program links against it by default. You access its contents with the std:: prefix, like std::vec::Vec or std::fs::read_to_string.
Rust also includes the prelude, a hidden use statement that the compiler inserts automatically into every module. The prelude brings the most frequently used types and traits into scope so you don't have to type std:: every time. Vec, String, Result, Option, Box, and the println! macro are all in the prelude.
When you see a type in Rust code without a std:: prefix, it's either in the prelude or defined locally. If you're unsure where a type comes from, check the prelude first. If it's not there, look in std::.
The prelude saves you keystrokes, but it doesn't hide the structure. When you see a type you don't recognize, check std:: to find where it lives.
A minimal tour
Here's a quick look at how the standard library types work together. This example uses prelude types directly and imports a collection that isn't in the prelude.
/// Demonstrates core std types and the prelude.
fn main() {
// Vec and String are in the prelude, so no std:: prefix needed.
// vec! is a macro that creates a Vec from a list of items.
let mut items = vec!["apple", "banana"];
items.push("cherry");
// HashMap requires an explicit import because it's not in the prelude.
use std::collections::HashMap;
let mut counts = HashMap::new();
for item in &items {
// entry API avoids double hashing on insert.
*counts.entry(item).or_insert(0) += 1;
}
// Result is in the prelude.
// to_uppercase returns a String.
let text = "hello world";
let upper = text.to_uppercase();
println!("{upper}");
}
When you compile this, the linker stitches your code to libstd.rlib. At runtime, std manages the allocator, the thread pool, and the I/O buffers. The prelude types like Vec allocate on the heap. HashMap uses a hashbrown-based structure for speed. Everything is designed to be zero-cost where possible, but std prioritizes safety and ergonomics over raw micro-optimizations. You get the speed without the pain.
Strings and slices: the memory model
Strings in Rust come in two flavors: String and &str. This distinction is the source of most beginner confusion, but it's also what makes string handling safe and fast.
String is an owned, growable buffer of UTF-8 bytes. It lives on the heap. You create it with String::from, String::new, or by calling .to_string() on a &str. When the String goes out of scope, the memory is freed.
&str is a borrowed view into string data. It doesn't own the data. It points to a slice of UTF-8 bytes somewhere else: inside a String, in a static string literal, or in a file. &str is the type you use for function arguments whenever possible.
/// Shows String vs &str usage.
fn main() {
// String owns the data on the heap.
let owned = String::from("hello world");
// &str borrows the data from the String.
let borrowed: &str = &owned;
// String literals are &str that live for the entire program.
let literal: &str = "static text";
// Slicing a String gives a &str.
let slice = &owned[0..5];
println!("{slice}");
}
Convention aside: prefer String::from("literal") over "literal".to_string() when creating a String from a literal. Both compile and both allocate, but String::from is more explicit about the allocation. to_string() relies on the Display trait, which can be slower and less obvious to readers.
Prefer &str in function arguments. It accepts both String and string literals without forcing the caller to allocate.
Slices: views without ownership
Slices are the universal view type in Rust. A slice &[T] is a borrowed reference to a contiguous sequence of elements of type T. It doesn't own the data. It just points to a start and a length.
Slices work with Vec, arrays, and even static data. When you pass &[T] to a function, you accept vectors, arrays, and other slices without caring where the data lives.
/// Demonstrates slice usage with different sources.
fn print_items(items: &[&str]) {
// &[&str] accepts Vec<&str>, arrays, and slices.
for item in items {
println!("{item}");
}
}
fn main() {
// Vec<&str>
let vec_items = vec!["one", "two"];
print_items(&vec_items);
// Array
let array_items = ["three", "four"];
print_items(&array_items);
// Slice of a Vec
let more = vec!["five", "six", "seven"];
print_items(&more[1..]);
}
Slices are the universal view. Pass &[T] instead of &Vec<T> to accept arrays, slices, and vectors alike.
Error handling: Option and Result
Rust has no null pointers and no exceptions. Instead, it uses Option<T> and Result<T, E> to represent the presence or absence of values and the success or failure of operations.
Option<T> is either Some(T) or None. Use it when a value might not exist. Result<T, E> is either Ok(T) or Err(E). Use it when an operation might fail.
/// Demonstrates Option and Result usage.
fn find_name(id: u32) -> Option<String> {
if id == 1 {
Some(String::from("Alice"))
} else {
None
}
}
fn parse_number(text: &str) -> Result<i32, std::num::ParseIntError> {
// parse() returns a Result.
text.parse()
}
fn main() {
// Match on Option.
match find_name(1) {
Some(name) => println!("Found {name}"),
None => println!("Not found"),
}
// Use ? to propagate errors.
let num = parse_number("42").unwrap();
println!("{num}");
}
Convention aside: use the ? operator to propagate errors in functions that return Result. It unwraps the Ok value or returns the Err immediately. It keeps error handling concise and readable.
The compiler forces you to handle None and Err cases. You can't ignore them. This eliminates entire classes of runtime crashes.
Realistic usage: parsing a config file
Here's a realistic example that ties together std::fs, std::io, std::path, Vec, String, and Result. It reads a configuration file and parses key-value pairs.
use std::fs;
use std::io;
use std::path::Path;
/// Reads a configuration file and returns key-value pairs.
/// Returns an error if the file is missing or malformed.
fn load_config(path: &Path) -> Result<Vec<(String, String)>, io::Error> {
// fs::read_to_string handles the file open, read, and close.
// ? propagates IO errors to the caller.
let content = fs::read_to_string(path)?;
let mut pairs = Vec::new();
for line in content.lines() {
// Skip empty lines and comments.
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
// Split on the first '=' only.
if let Some((key, value)) = line.split_once('=') {
pairs.push((key.trim().to_string(), value.trim().to_string()));
}
}
Ok(pairs)
}
fn main() {
// eprintln! writes to stderr, which is the convention for errors.
match load_config(Path::new("config.txt")) {
Ok(config) => println!("Loaded {} settings", config.len()),
Err(e) => eprintln!("Failed to load config: {e}"),
}
}
This code uses std::fs for file operations, std::io::Error for I/O errors, and std::path::Path for path manipulation. It returns a Result to signal success or failure. The ? operator keeps the error handling clean.
Convention aside: use eprintln! for error messages. It writes to standard error, which can be redirected separately from standard output. This follows Unix conventions and makes logging easier.
Pitfalls and compiler errors
The standard library is safe, but it has traps for the unwary. Here are the most common pitfalls.
If you create a String inside a function and try to return a &str slice of it, the compiler stops you with E0515 (cannot return value referencing local data). The String gets dropped at the end of the function, leaving the slice pointing to garbage. Return the String itself, or borrow from the caller.
If you try to move a value into a thread closure without ownership, the compiler yells. E0382 (use of moved value) appears if you try to use a variable after passing it to thread::spawn. The closure takes ownership. You can't use the original variable afterward.
If you try to use a type that doesn't implement a required trait, you get E0277 (trait bound not satisfied). For example, if you try to put a type that doesn't implement Clone into a collection that requires it, the compiler rejects you. Check the trait bounds in the documentation.
Threads own their data. If you need to share, reach for Arc, not a raw copy.
Choosing the right tool
The standard library offers multiple options for many tasks. Pick the one that matches your needs.
Use Vec<T> when you need a dynamic array that grows and shrinks. It's the default collection for ordered data. Use VecDeque when you need efficient insertion and removal from both ends. Use BinaryHeap when you need a priority queue. Use BTreeMap when you need sorted keys or range queries. Use HashMap<K, V> when you need fast lookups by key. It's the go-to for associative data.
Use String when you need an owned, growable string. Use &str when you need a borrowed view of string data. Prefer &str in function arguments to accept both owned and borrowed strings.
Use std::fs when you need to read or write files. It wraps OS file operations in safe Rust types. Use std::io when you're working with streams, readers, and writers. It provides the traits and types for buffered I/O. Use std::path when you're manipulating file paths. It handles OS-specific separators and extensions correctly.
Use std::thread when you need true parallelism on multiple CPU cores. It spawns OS threads, which are heavy but powerful. Use std::sync when you need to share data between threads safely. Mutex and RwLock live here. Use std::net when you need TCP or UDP sockets. It provides safe wrappers around network I/O.
Pick the type that matches your access pattern. Vec for order, HashMap for lookup, BTreeMap for sorted keys.