Tech Guides
01

Quick Reference

Essential Rust commands, syntax patterns, and common types at a glance.

Common Cargo Commands

Command Description
cargo new project_name Create a new binary project with directory and Git init
cargo build Compile the current project (debug mode)
cargo run Build and execute the current project
cargo test Run all tests (unit, integration, doc tests)
cargo check Fast syntax and type check without producing a binary
cargo clippy Run the linter for idiomatic Rust suggestions
cargo fmt Auto-format code with rustfmt
cargo doc --open Generate and open API documentation
cargo add serde Add a crate dependency to Cargo.toml
rustup update Update Rust toolchain to latest stable

Quick Syntax Reference

// Variables
let x = 5;                     // immutable binding
let mut y = 10;                // mutable binding
const MAX: u32 = 100_000;     // compile-time constant

// Functions
fn add(a: i32, b: i32) -> i32 {
    a + b                       // implicit return (no semicolon)
}

// If / Else (it's an expression!)
let status = if score > 90 { "excellent" } else { "good" };

// Loops
loop {                          // infinite loop
    break;                      // exit loop
}

while condition {               // conditional loop
    // ...
}

for item in collection {        // iterator loop
    println!("{item}");
}

for i in 0..10 {                // range loop (0 to 9)
    println!("{i}");
}

// Match (exhaustive pattern matching)
match value {
    1 => println!("one"),
    2 | 3 => println!("two or three"),
    4..=9 => println!("four through nine"),
    _ => println!("something else"),
}

Common Types Quick Reference

Type Description Example
i32 32-bit signed integer (default integer type) let x: i32 = -42;
f64 64-bit floating point (default float type) let pi: f64 = 3.14159;
bool Boolean value let active: bool = true;
char Unicode scalar value (4 bytes) let c: char = 'R';
String Heap-allocated, growable UTF-8 string let s = String::from("hello");
&str String slice (borrowed reference to UTF-8 data) let s: &str = "hello";
Vec<T> Growable array (heap-allocated) let v: Vec<i32> = vec![1, 2, 3];
Option<T> Optional value: Some(T) or None let n: Option<i32> = Some(5);
Result<T, E> Success or error: Ok(T) or Err(E) let r: Result<i32, String> = Ok(42);
HashMap<K, V> Hash map (requires use std::collections::HashMap) let mut m = HashMap::new();

Ownership Quick Rules

// Rule 1: Each value has exactly ONE owner
let s1 = String::from("hello");

// Rule 2: When the owner goes out of scope, the value is dropped
{
    let s = String::from("temporary");
}   // s is dropped here, memory freed

// Rule 3: Ownership can be transferred (moved), not duplicated
let s2 = s1;           // s1 is MOVED to s2
// println!("{s1}");   // ERROR: s1 is no longer valid
println!("{s2}");      // OK: s2 owns the data

// Borrowing lets you use a value without taking ownership
let s3 = String::from("borrowed");
let len = calculate_length(&s3);  // &s3 borrows, s3 still valid
println!("{s3} has length {len}");

Common Operations

// Printing
println!("Hello, world!");            // print with newline
println!("x = {}, y = {}", x, y);    // positional args
println!("x = {x}, y = {y}");        // inline variables (Rust 2021+)
println!("debug: {:?}", value);       // Debug format
println!("pretty: {:#?}", value);     // Pretty Debug format
eprintln!("error: {}", msg);          // print to stderr
let s = format!("name: {}", name);    // format to String

// Vector creation
let v1 = vec![1, 2, 3];              // vec! macro
let v2: Vec<i32> = Vec::new();       // empty vector
let v3 = vec![0; 10];                // 10 zeros

// Error handling shortcuts
let val = result.unwrap();            // panic on Err
let val = result.expect("failed");    // panic with message
let val = result?;                    // propagate error (returns early)
let val = option.unwrap_or(default);  // fallback value
let val = option.unwrap_or_default(); // use Default trait

Rust Edition Note

This guide covers Rust 2021 edition and later. Most syntax is edition-independent, but certain features like let-else and async improvements require 2021+. The 2024 edition (Rust 1.85+) brings further refinements to gen blocks and unsafe ergonomics.

02

Setup & Installation

Installing Rust via rustup, managing toolchains, configuring your editor, and creating your first project.

Installing via Rustup

Rustup is the official Rust toolchain installer. It manages Rust versions, components, and cross-compilation targets. One command installs everything you need.

# Install Rust (Linux / macOS / WSL)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Follow the prompts, then reload your shell:
source "$HOME/.cargo/env"

# Verify installation
rustc --version          # e.g. rustc 1.85.0 (4d91de4e4 2025-02-17)
cargo --version          # e.g. cargo 1.85.0 (d73d2caf9 2024-12-31)
rustup --version         # e.g. rustup 1.27.1

What Gets Installed

rustup installs three key tools: rustc (the compiler), cargo (the build system and package manager), and rustfmt (the code formatter). Everything lives under ~/.cargo/ and ~/.rustup/.

Toolchain Management

Rustup lets you install and switch between stable, beta, and nightly toolchains, and add components like clippy, rust-analyzer, and cross-compilation targets.

rustup show
Display installed toolchains, active toolchain, and host triple
rustup update
Update all installed toolchains to their latest versions
rustup default nightly
Switch default toolchain to nightly (use stable to switch back)
rustup component add clippy
Add the Clippy linter component to your active toolchain
rustup component add rust-analyzer
Add the rust-analyzer language server for IDE support
rustup target add wasm32-unknown-unknown
Add a cross-compilation target (e.g., WebAssembly)
# Pin a project to a specific toolchain with a rust-toolchain.toml file:
# rust-toolchain.toml
[toolchain]
channel = "1.85.0"
components = ["rustfmt", "clippy", "rust-analyzer"]
targets = ["wasm32-unknown-unknown"]

Editor Setup

rust-analyzer is the official language server for Rust. It provides code completion, inline type hints, go-to-definition, refactoring, and real-time error checking.

  • VS Code: Install the "rust-analyzer" extension (not the deprecated "Rust" extension). It uses the rust-analyzer binary from your toolchain.
  • Neovim: Use nvim-lspconfig with rust_analyzer. Add rustfmt as the formatter via conform.nvim or null-ls.
  • IntelliJ / CLion: Install the "Rust" plugin by JetBrains. It bundles its own analysis engine alongside rust-analyzer support.
  • Helix / Zed: Both have built-in rust-analyzer support out of the box.

Performance Tip

For large projects, set "rust-analyzer.check.command": "clippy" in your editor settings. This runs clippy instead of cargo check on save, catching more issues. If builds feel slow, add "rust-analyzer.cargo.buildScripts.enable": false temporarily.

Creating Your First Project

# Create a new binary project
cargo new my_app            # Creates my_app/ with src/main.rs
cd my_app
cargo run                   # Compiles and runs: "Hello, world!"

# Create a new library project
cargo new my_lib --lib      # Creates my_lib/ with src/lib.rs

# Initialize in an existing directory
mkdir my_project && cd my_project
cargo init                  # Creates Cargo.toml and src/main.rs
cargo init --lib            # Or initialize as a library

After cargo new, your project structure looks like this:

my_app/
├── Cargo.toml              # Project manifest (name, version, dependencies)
├── Cargo.lock              # Exact dependency versions (auto-generated)
├── src/
│   └── main.rs             # Entry point: fn main() { }
├── tests/                  # Integration tests (optional)
├── benches/                # Benchmarks (optional)
├── examples/               # Example programs (optional)
└── .gitignore              # Pre-configured for Rust

Cargo.toml Structure

The Cargo.toml manifest file defines your project metadata, dependencies, and build configuration. It uses TOML format.

[package]
name = "my_app"
version = "0.1.0"
edition = "2021"             # Rust edition (2015, 2018, 2021, 2024)
authors = ["Your Name <you@example.com>"]
description = "A short description of the project"
license = "MIT OR Apache-2.0"
rust-version = "1.75"        # Minimum supported Rust version (MSRV)

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }

[build-dependencies]
cc = "1.0"

[[bin]]
name = "my_app"
path = "src/main.rs"

[profile.release]
opt-level = 3                # Maximum optimization
lto = true                   # Link-time optimization
strip = true                 # Strip debug symbols

Edition Configuration

Rust editions let the language evolve without breaking existing code. Each edition is a checkpoint that enables new syntax and behaviors. Code from different editions can interoperate seamlessly.

Edition Released Key Features
2015 Rust 1.0 Original edition, explicit extern crate, older module paths
2018 Rust 1.31 Module system overhaul, async/await syntax, NLL borrow checker
2021 Rust 1.56 Disjoint closure captures, IntoIterator for arrays, new prelude additions
2024 Rust 1.85 Refined unsafe rules, gen blocks, impl Trait in let, lifetime capture rules
# Automatically migrate your code to a newer edition:
cargo fix --edition          # Apply automated fixes
# Then update edition = "2024" in Cargo.toml

Which Edition to Use

New projects should use edition = "2021" for maximum ecosystem compatibility, or edition = "2024" if you are on Rust 1.85+ and want the latest features. Editions are backward-compatible: a 2024-edition crate can depend on 2015-edition crates with no issues.

03

Core Types & Variables

Rust's type system is one of its greatest strengths. Every value has a type known at compile time, enabling fearless refactoring and zero-cost abstractions.

Integer Types

Rust provides fixed-size signed and unsigned integers. The default integer type (when Rust must infer) is i32.

Signed Unsigned Size Range (Signed)
i8 u8 1 byte -128 to 127
i16 u16 2 bytes -32,768 to 32,767
i32 u32 4 bytes -2.1 billion to 2.1 billion
i64 u64 8 bytes -9.2 quintillion to 9.2 quintillion
i128 u128 16 bytes -1.7×1038 to 1.7×1038
isize usize pointer-sized Platform-dependent (32 or 64 bit)
// Integer literals support various formats
let decimal = 98_222;           // underscores for readability
let hex = 0xff;                 // hexadecimal
let octal = 0o77;               // octal
let binary = 0b1111_0000;       // binary
let byte = b'A';                // byte literal (u8 only)

// Type suffixes
let x = 42u8;                   // explicit u8
let y = 1_000_000i64;           // explicit i64

// usize is used for indexing and collection sizes
let length: usize = vec.len();
let element = &array[index];     // index must be usize

Integer Overflow

In debug mode, Rust panics on integer overflow. In release mode (cargo build --release), overflow wraps silently using two's complement. Use checked_add(), wrapping_add(), saturating_add(), or overflowing_add() for explicit control.

Floating-Point Types

let x = 2.0;                    // f64 (default)
let y: f32 = 3.0;              // f32 (explicit)

// Arithmetic
let sum = 5.0 + 10.0;          // f64
let quotient = 56.7 / 32.2;
let truncated = -5.0_f64 / 3.0; // -1.666...
let remainder = 43.0 % 5.0;    // 3.0

// f64: 64-bit double precision (IEEE 754)
// f32: 32-bit single precision (IEEE 754)
// f64 is default because on modern CPUs it's roughly the same speed as f32

Bool & Char

// Boolean: one byte, true or false
let t: bool = true;
let f = false;                  // type inferred as bool

// Char: 4 bytes, represents a Unicode Scalar Value
let c = 'z';
let heart = '❤';
let crab = '🦀';                  // Ferris!
let unicode = '\u{1F600}';      // Unicode escape

// char can represent any Unicode scalar value (U+0000 to U+D7FF, U+E000 to U+10FFFF)
// This is more than just ASCII — accented letters, CJK, emoji all work

Type Inference & Annotations

Rust has powerful type inference. In most cases you can omit type annotations and the compiler will figure it out from context. When it cannot (or for clarity), add explicit annotations.

// Rust infers types from context
let x = 5;                     // inferred as i32
let y = 3.14;                  // inferred as f64
let name = "Ferris";           // inferred as &str
let numbers = vec![1, 2, 3];   // inferred as Vec<i32>

// Sometimes inference needs help
let guess: u32 = "42".parse().expect("Not a number!");
// Without :u32 the compiler can't know which numeric type to parse into

// Turbofish syntax — specify type parameters inline
let parsed = "42".parse::<u32>().unwrap();
let collected: Vec<i32> = (0..10).collect();
// or equivalently:
let collected = (0..10).collect::<Vec<i32>>();

Bindings: let, let mut, const, static

// let — immutable by default
let x = 5;
// x = 6;                      // ERROR: cannot assign twice to immutable variable

// let mut — mutable binding
let mut y = 5;
y = 6;                          // OK

// const — compile-time constant, must have explicit type
const MAX_POINTS: u32 = 100_000;
const PI: f64 = 3.141_592_653_589_793;
// Constants are inlined at every usage site. No fixed memory address.
// They must be evaluable at compile time (const expressions only).

// static — global variable with a fixed memory address
static LANGUAGE: &str = "Rust";
static mut COUNTER: u32 = 0;   // mutable statics require unsafe to access

// static vs const:
//   const: inlined, no memory address, can be used in patterns
//   static: fixed address, one instance, lives for entire program ('static lifetime)

Prefer const Over static

Use const for values that are known at compile time and don't need a fixed address. Use static only when you need a global variable with a stable memory location (e.g., for FFI). Mutable statics (static mut) are unsafe and should be avoided in favor of std::sync::Mutex or atomic types.

Shadowing

Rust lets you re-declare a variable with let, creating a new binding that shadows the previous one. Unlike mutation, shadowing can change the type.

// Shadowing — creating a new binding with the same name
let x = 5;
let x = x + 1;                 // x is now 6 (new binding)
let x = x * 2;                 // x is now 12 (new binding)

// Shadowing can change types — mutation cannot
let spaces = "   ";             // &str
let spaces = spaces.len();      // usize — different type, same name

// With mut, you CANNOT change types:
// let mut spaces = "   ";
// spaces = spaces.len();       // ERROR: expected &str, found usize

// Shadowing is common in transformations:
let input = "42";
let input: u32 = input.parse().unwrap();
// Reusing the name makes it clear this is the "same concept" in a new form

// Shadowing in inner scopes
let x = 5;
{
    let x = x * 2;             // inner x = 10
    println!("{x}");            // prints 10
}
println!("{x}");                // prints 5 (outer x unchanged)

String vs &str

This distinction is fundamental to Rust. Understanding when to use each is key to productive Rust programming.

String (Owned)

Heap-allocated, growable, owned UTF-8 string. You use this when you need to own or modify string data.

let mut s = String::new();
s.push_str("hello");
s.push(' ');
s += "world";

let s = String::from("hello");
let s = "hello".to_string();
let s = format!("{} {}", "hello", "world");

&str (Borrowed Slice)

An immutable reference to a UTF-8 string. String literals are &str. Use this for read-only access and function parameters.

let s: &str = "hello world";
let first_word: &str = &s[0..5];

// Function parameters should prefer &str:
fn greet(name: &str) {
    println!("Hello, {name}!");
}
greet("world");                // &str works
greet(&my_string);             // &String coerces to &str
// Conversions between String and &str
let owned: String = "hello".to_string();    // &str -> String
let owned: String = String::from("hello");  // &str -> String
let borrowed: &str = &owned;                 // String -> &str (deref coercion)
let borrowed: &str = owned.as_str();        // String -> &str (explicit)

// Common pattern: accept &str, return String
fn capitalize(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(c) => c.to_uppercase().to_string() + chars.as_str(),
    }
}

Rule of Thumb

Use &str for function parameters (accept the broadest input). Use String when you need ownership, such as storing in a struct, returning from a function, or building strings dynamically. Rust's deref coercion means a &String can be passed anywhere a &str is expected.

Tuples & Arrays

// Tuples — fixed-size, mixed-type compound values
let tup: (i32, f64, &str) = (500, 6.4, "hello");
let (x, y, z) = tup;           // destructuring
let first = tup.0;             // index access
let second = tup.1;

// Unit tuple — the "void" type in Rust
let unit: () = ();             // functions without a return type return ()

// Arrays — fixed-size, same-type, stack-allocated
let arr = [1, 2, 3, 4, 5];           // [i32; 5]
let arr: [i32; 5] = [1, 2, 3, 4, 5]; // explicit type
let zeros = [0; 10];                  // [0, 0, 0, ...] (10 zeros)

let first = arr[0];            // index access
let len = arr.len();           // 5

// Arrays are fixed-size (known at compile time)
// For dynamic-size collections, use Vec<T>

// Slices — references to a contiguous portion of an array or Vec
let slice: &[i32] = &arr[1..3];  // [2, 3]
let full_slice: &[i32] = &arr;    // entire array as slice

// Slices are very common in function signatures:
fn sum(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
}
sum(&arr);                      // pass array as slice
sum(&vec![1, 2, 3]);           // pass Vec as slice

Type Casting with as

Rust does not implicitly convert between numeric types. Use as for explicit casts, and be aware of potential data loss.

// Numeric casts
let x: i32 = 42;
let y: f64 = x as f64;         // i32 -> f64 (safe, no loss)
let z: i64 = x as i64;         // i32 -> i64 (safe, widening)

let a: f64 = 3.99;
let b: i32 = a as i32;         // f64 -> i32 = 3 (truncates, no rounding!)

let big: i32 = 300;
let small: u8 = big as u8;     // 300 overflows u8 -> wraps to 44

let c: u8 = 65;
let ch: char = c as char;      // u8 -> char = 'A'

// For safe conversions, prefer From/Into traits:
let x: i64 = i64::from(42i32);     // guaranteed safe
let x: i64 = 42i32.into();         // same thing via Into

// TryFrom/TryInto for fallible conversions:
use std::convert::TryFrom;
let big: i64 = 1_000_000;
let small: Result<i32, _> = i32::try_from(big);  // Ok(1000000)
let too_big: i64 = 10_000_000_000;
let fail: Result<i32, _> = i32::try_from(too_big); // Err(...)

Beware of as Casts

The as keyword performs C-style casts and can silently truncate or wrap values. Prefer From/Into for safe widening conversions and TryFrom/TryInto for narrowing conversions that might fail. The clippy::cast_possible_truncation lint can help catch risky casts.

04

Ownership & Borrowing

Rust's ownership system is the foundation of its memory safety guarantees. No garbage collector, no manual free — the compiler enforces safe memory management at compile time.

The Three Ownership Rules

Every piece of data in Rust has exactly one owner. These three rules govern how memory is managed without a garbage collector.

Rule Description
Rule 1 Each value in Rust has exactly one owner at a time
Rule 2 When the owner goes out of scope, the value is dropped (memory freed)
Rule 3 Ownership can be transferred (moved) to another variable or function
// Rule 1: Each value has one owner
let s1 = String::from("hello");    // s1 owns this String

// Rule 2: Value is dropped when owner goes out of scope
{
    let s = String::from("temporary");
    println!("{s}");
}   // s goes out of scope here, String is dropped, memory freed

// Rule 3: Ownership can be transferred (moved)
let s1 = String::from("hello");
let s2 = s1;                       // ownership MOVES from s1 to s2
// println!("{s1}");               // ERROR: value borrowed after move
println!("{s2}");                   // OK: s2 is the new owner

Move Semantics

When you assign a heap-allocated value to another variable or pass it to a function, ownership moves. The original variable becomes invalid. This prevents double-free bugs at compile time.

// String is heap-allocated, so assignment MOVES
let s1 = String::from("hello");
let s2 = s1;                       // s1's data is moved to s2
// s1 is now invalid — the compiler prevents use-after-move
// println!("{s1}");               // ERROR: borrow of moved value: `s1`

// Moving also happens when passing to functions
fn take_ownership(s: String) {
    println!("I own: {s}");
}   // s is dropped here

let name = String::from("Ferris");
take_ownership(name);
// println!("{name}");             // ERROR: name was moved into the function

// Vec, HashMap, Box, and other heap types also move
let v1 = vec![1, 2, 3];
let v2 = v1;                       // v1 is moved
// println!("{:?}", v1);           // ERROR: value used after move

Why i32 Does Not Move

Simple scalar types like i32, f64, bool, and char implement the Copy trait. When you assign a Copy type, the value is duplicated on the stack (a cheap bitwise copy), so the original remains valid. Only heap-allocated types (like String, Vec, Box) actually move.

// i32 implements Copy, so assignment copies (not moves)
let x = 42;
let y = x;                         // x is COPIED, not moved
println!("x = {x}, y = {y}");      // both valid!

// bool, char, f64 — all Copy types
let a = true;
let b = a;                         // copy
println!("{a} and {b}");            // both valid

// References (&T) are also Copy — copying a reference is cheap
let s = String::from("hello");
let r1 = &s;
let r2 = r1;                       // copies the reference, not the data
println!("{r1} and {r2}");          // both valid

Clone vs Copy

Copy is an implicit, cheap bitwise duplication. Clone is an explicit, potentially expensive deep duplication. They serve different purposes.

Copy (Implicit)

Bitwise duplication on the stack. Happens automatically on assignment. Must be cheap and trivial. The type cannot also implement Drop.

// Types that implement Copy:
// i8, i16, i32, i64, i128, isize
// u8, u16, u32, u64, u128, usize
// f32, f64
// bool, char
// &T (shared references)
// (A, B) if A and B are Copy
// [T; N] if T is Copy

let x: i32 = 42;
let y = x;     // implicit copy
// both x and y are valid

Clone (Explicit)

Deep duplication that may allocate memory. Must be called explicitly with .clone(). Can be expensive for large data structures.

// Clone when you need a duplicate
// of a non-Copy type
let s1 = String::from("hello");
let s2 = s1.clone();  // explicit deep copy
println!("{s1} and {s2}"); // both valid

let v1 = vec![1, 2, 3];
let v2 = v1.clone();  // clones all elements
println!("{v1:?} and {v2:?}");

// Clone copies the heap data too
// (not just the stack pointer)
// Deriving Copy and Clone for your own types
#[derive(Debug, Copy, Clone)]
struct Point {
    x: f64,
    y: f64,
}

let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1;                       // implicit copy because Point is Copy
println!("{:?} and {:?}", p1, p2);  // both valid

// You can only derive Copy if ALL fields are Copy
// This will NOT compile:
// #[derive(Copy, Clone)]
// struct Name {
//     value: String,   // String is not Copy!
// }

// But you can always derive Clone alone
#[derive(Debug, Clone)]
struct Person {
    name: String,       // String is Clone but not Copy
    age: u32,
}

let p1 = Person { name: "Alice".into(), age: 30 };
let p2 = p1.clone();               // explicit clone required
println!("{:?}", p2);

References: &T (Shared / Immutable)

A shared reference &T lets you read data without taking ownership. Multiple shared references can coexist simultaneously. The data cannot be modified through a shared reference.

// Borrow with & to read without taking ownership
let s = String::from("hello");
let len = calculate_length(&s);     // borrow s
println!("{s} has length {len}");    // s is still valid!

fn calculate_length(s: &String) -> usize {
    s.len()
}   // s goes out of scope, but it doesn't own the data, so nothing is dropped

// Multiple shared references are allowed simultaneously
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{r1}, {r2}, {r3}");       // all valid at the same time

// Shared references are immutable — you cannot modify through them
let s = String::from("hello");
let r = &s;
// r.push_str(" world");           // ERROR: cannot borrow as mutable

// Functions should prefer &str over &String (more general)
fn print_greeting(name: &str) {     // accepts &str and &String
    println!("Hello, {name}!");
}
let owned = String::from("world");
print_greeting(&owned);             // &String coerces to &str
print_greeting("world");            // &str directly

References: &mut T (Exclusive / Mutable)

A mutable reference &mut T lets you modify borrowed data. Only one mutable reference to a particular piece of data can exist at a time, preventing data races at compile time.

// Mutable borrow with &mut
let mut s = String::from("hello");
change(&mut s);
println!("{s}");                    // prints "hello, world"

fn change(s: &mut String) {
    s.push_str(", world");
}

// Only ONE mutable reference at a time
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s;               // ERROR: cannot borrow `s` as mutable more than once
r1.push_str(" world");
println!("{r1}");

// Mutable references in different scopes are fine
let mut s = String::from("hello");
{
    let r1 = &mut s;
    r1.push_str(" world");
}   // r1 goes out of scope here
let r2 = &mut s;                   // OK: r1 is no longer active
r2.push_str("!");
println!("{s}");                    // "hello world!"

Borrowing Rules

The borrow checker enforces two critical rules that prevent data races at compile time. These rules apply within the non-lexical lifetime (NLL) of each reference.

Situation Allowed? Reason
Multiple &T at once Yes Read-only access is safe to share
Single &mut T alone Yes Exclusive write access is safe
&T and &mut T at once No Readers would see inconsistent data
Multiple &mut T at once No Concurrent writes cause data races
// EITHER multiple shared references OR one mutable reference, never both
let mut s = String::from("hello");

// This is fine: multiple shared references
let r1 = &s;
let r2 = &s;
println!("{r1} and {r2}");

// This is fine: one mutable reference (after shared refs are done)
let r3 = &mut s;                   // OK: r1 and r2 are no longer used
r3.push_str(" world");

// NLL (Non-Lexical Lifetimes): references end at their last USE, not scope end
let mut data = vec![1, 2, 3];
let first = &data[0];              // shared borrow starts
println!("first = {first}");        // last use of `first`
// shared borrow ends here (NLL)
data.push(4);                       // mutable borrow is OK now
println!("{data:?}");

// This would NOT compile — shared and mutable borrows overlap
// let mut data = vec![1, 2, 3];
// let first = &data[0];
// data.push(4);                    // ERROR: cannot borrow as mutable
// println!("{first}");             // shared borrow still alive here

The Borrow Checker is Your Friend

When the borrow checker rejects your code, it is preventing a real category of bugs: dangling pointers, data races, and use-after-free. Rather than fighting it, restructure your code: clone to break sharing, use scoped borrows, or refactor to reduce overlapping lifetimes. Over time, you will learn to write code that flows naturally within these rules.

Dangling References

Rust prevents dangling references at compile time. You can never have a reference that outlives the data it points to.

// The compiler prevents dangling references
// This will NOT compile:
// fn dangle() -> &String {
//     let s = String::from("hello");
//     &s                             // ERROR: returns a reference to dropped data
// }   // s is dropped here, reference would be dangling

// Solution: return the owned value instead
fn no_dangle() -> String {
    let s = String::from("hello");
    s                                // ownership is moved out, no dangling reference
}

// Another example of what the compiler catches:
// let reference;
// {
//     let value = 42;
//     reference = &value;           // ERROR: `value` does not live long enough
// }   // value is dropped
// println!("{reference}");          // would be a dangling reference

// The fix: ensure the data lives long enough
let value = 42;
let reference = &value;
println!("{reference}");             // OK: value is still in scope

Real-World Ownership Patterns

These common patterns show how ownership works in practice with function parameters, return values, and struct fields.

// Pattern 1: Borrow for read-only access (most common)
fn count_words(text: &str) -> usize {
    text.split_whitespace().count()
}
let article = String::from("Rust is great");
let count = count_words(&article);   // borrow — article still valid
println!("{article} has {count} words");

// Pattern 2: Take ownership when you need to store or consume
fn into_uppercase(s: String) -> String {
    s.to_uppercase()                   // consumes s, returns new String
}
let name = String::from("ferris");
let upper = into_uppercase(name);     // name is moved
// println!("{name}");                // ERROR: name was moved
println!("{upper}");                   // "FERRIS"

// Pattern 3: Mutable borrow to modify in place
fn add_greeting(buffer: &mut String, name: &str) {
    buffer.push_str("Hello, ");
    buffer.push_str(name);
    buffer.push('!');
}
let mut msg = String::new();
add_greeting(&mut msg, "Ferris");
println!("{msg}");                     // "Hello, Ferris!"

// Pattern 4: Return a newly created value (caller gets ownership)
fn create_user(name: &str, age: u32) -> String {
    format!("{name} (age {age})")
}
let user = create_user("Alice", 30);  // caller owns the String
println!("{user}");

// Pattern 5: Multiple return values with tuple
fn first_and_last(s: &str) -> (char, char) {
    let first = s.chars().next().unwrap();
    let last = s.chars().last().unwrap();
    (first, last)
}
let (f, l) = first_and_last("Rust");
println!("First: {f}, Last: {l}");

Choosing the Right Parameter Type

Use &T when you only need to read. Use &mut T when you need to modify. Take T (owned) when the function needs to store the data or the caller is done with it. For strings specifically, prefer &str over &String as the parameter type because it accepts both &String and &str thanks to deref coercion.

Lifetimes

Lifetimes are Rust's way of tracking how long references are valid. Most of the time the compiler infers them automatically, but sometimes you need explicit annotations to tell the compiler how reference lifetimes relate to each other.

// Basic lifetime annotation syntax: 'a (tick + name)
// Lifetimes go in angle brackets after the function name

// This function returns a reference — but to which input?
// The lifetime annotation 'a tells the compiler:
// "the returned reference lives as long as both inputs"
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

let result;
let string1 = String::from("long string");
{
    let string2 = String::from("xyz");
    result = longest(string1.as_str(), string2.as_str());
    println!("{result}");         // OK: both strings alive here
}
// println!("{result}");          // ERROR if string2 is used as result:
                                  // string2 doesn't live long enough
// Lifetime elision rules — when the compiler infers lifetimes for you
//
// Rule 1: Each reference parameter gets its own lifetime
//   fn foo(x: &str, y: &str)  =>  fn foo<'a, 'b>(x: &'a str, y: &'b str)
//
// Rule 2: If there's exactly one input lifetime, it's assigned to all outputs
//   fn foo(x: &str) -> &str   =>  fn foo<'a>(x: &'a str) -> &'a str
//
// Rule 3: If one parameter is &self or &mut self, its lifetime is
//         assigned to all output lifetimes
//   fn method(&self, x: &str) -> &str  =>  output gets self's lifetime
//
// If these rules don't fully determine output lifetimes, you must annotate.

// Examples where elision works (no annotations needed):
fn first_word(s: &str) -> &str {                // Rule 1 + Rule 2
    s.split_whitespace().next().unwrap_or("")
}

impl MyStruct {
    fn name(&self) -> &str {                     // Rule 3
        &self.name
    }
}
// The 'static lifetime — lives for the entire program duration
let s: &'static str = "I live forever";  // string literals are 'static

// 'static is also used in trait bounds:
// T: 'static means T contains no non-static references
// (it either owns all its data or only has 'static references)
fn print_static(s: &'static str) {
    println!("{s}");
}

// Common misconception: 'static does NOT mean "allocated forever"
// It means "CAN live forever" — the data won't become invalid
// String literals are embedded in the binary, so they're always valid

Lifetime Annotations in Structs

When a struct holds a reference, it needs a lifetime annotation. This tells the compiler that the struct cannot outlive the data it references.

// A struct that borrows data — must declare a lifetime
struct Excerpt<'a> {
    text: &'a str,             // borrowed reference to a string slice
}

// The struct cannot outlive the data it references
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence;
{
    let excerpt = Excerpt {
        text: novel.split('.').next().unwrap(),
    };
    first_sentence = excerpt.text;  // OK: novel still alive
}
println!("{first_sentence}");       // OK: novel outlives this scope

// impl blocks for structs with lifetimes
impl<'a> Excerpt<'a> {
    // Lifetime elision Rule 3 applies: output gets &self's lifetime
    fn announce(&self, announcement: &str) -> &str {
        println!("Attention: {announcement}");
        self.text
    }
}

// Multiple lifetimes in a struct
struct Split<'a, 'b> {
    left: &'a str,
    right: &'b str,
}

// Common pattern: struct with lifetime + owned data
struct Config<'a> {
    name: String,              // owned — no lifetime needed
    description: &'a str,     // borrowed — needs lifetime
}

Common Lifetime Patterns

Pattern Signature Compiler Infers?
Single input ref, output ref fn f(x: &str) -> &str Yes (Rule 2)
Method returning ref to self fn f(&self) -> &Type Yes (Rule 3)
Two inputs, one output fn f<'a>(x: &'a str, y: &'a str) -> &'a str No — must annotate
Struct holding a reference struct S<'a> { field: &'a str } No — must annotate
No references in output fn f(x: &str, y: &str) -> String N/A — no output lifetime needed
Static string return fn f() -> &'static str No — must write 'static explicitly
// Tip: when you're unsure about lifetimes, try removing the annotations
// and let the compiler tell you if they're needed. If it compiles
// without them, elision is handling it.

// Tip: if lifetime annotations get complex, consider whether you
// should own the data instead of borrowing it:
//   &str -> String
//   &[T] -> Vec<T>
//   &T   -> T or Box<T>
// Owned data has no lifetime constraints.

Common Borrow Checker Errors

Borrow Checker Survival Guide

The borrow checker is your ally, not your enemy. Here are the most common errors and how to resolve them:

Error Cause Fix
use of moved value Value was moved to another variable or function Use .clone(), pass a reference (&val), or restructure to avoid the move
cannot borrow as mutable Trying to mutate while an immutable reference exists Ensure immutable borrows are used (and dropped) before mutating; narrow borrow scopes
cannot borrow as mutable more than once Two &mut references to the same data Use one &mut at a time; split data into separate fields; use Cell/RefCell for interior mutability
does not live long enough A reference outlives the data it points to Extend the data's lifetime (move binding to outer scope) or return an owned value instead
cannot return reference to local variable Returning &T where T is created inside the function Return the owned value (T or String instead of &str)
cannot move out of borrowed content Trying to take ownership of data behind a reference Use .clone(), .to_owned(), or work with the reference instead
// Example: iterating and mutating a collection
let mut scores = vec![1, 2, 3, 4, 5];

// WRONG: cannot borrow as mutable while iterating
// for score in &scores {
//     if *score > 3 {
//         scores.push(*score * 2);   // ERROR: mutable borrow during iteration
//     }
// }

// FIX: collect changes first, then apply
let additions: Vec<i32> = scores.iter()
    .filter(|&&s| s > 3)
    .map(|&s| s * 2)
    .collect();
scores.extend(additions);

// Or use retain/drain/indices to modify in place:
scores.retain(|&x| x <= 3);   // remove elements matching predicate
05

Structs & Enums

Custom data types are the building blocks of Rust programs. Structs group related fields together; enums model values that can be one of several variants.

Named Structs

A struct groups related data under a single type name. Each field has a name and a type. Struct instances are created by specifying values for every field.

// Defining a struct
struct User {
    username: String,
    email: String,
    age: u32,
    active: bool,
}

// Creating an instance — all fields must be specified
let user = User {
    username: String::from("ferris"),
    email: String::from("ferris@rust-lang.org"),
    age: 10,
    active: true,
};

// Accessing fields with dot notation
println!("Name: {}", user.username);
println!("Active: {}", user.active);

// Mutable struct — all fields become mutable (Rust has no per-field mutability)
let mut user = User {
    username: String::from("ferris"),
    email: String::from("ferris@rust-lang.org"),
    age: 10,
    active: true,
};
user.email = String::from("new@example.com");
user.age += 1;
// Field init shorthand — when variable names match field names
fn build_user(username: String, email: String) -> User {
    User {
        username,                   // shorthand for username: username
        email,                      // shorthand for email: email
        age: 0,
        active: true,
    }
}

// Struct update syntax — create from another instance
let user2 = User {
    email: String::from("other@example.com"),
    ..user                          // remaining fields from `user`
};
// WARNING: String fields are moved from `user`, so user.username is invalid
// but user.age and user.active are still valid (they implement Copy)

Tuple Structs & Unit Structs

Tuple structs have unnamed fields accessed by index. Unit structs have no fields at all. Both are useful for creating distinct types.

// Tuple structs — named tuples for type distinction
struct Color(u8, u8, u8);
struct Point3D(f64, f64, f64);

let red = Color(255, 0, 0);
let origin = Point3D(0.0, 0.0, 0.0);

// Access by index
println!("R: {}, G: {}, B: {}", red.0, red.1, red.2);
println!("x: {}", origin.0);

// Destructuring
let Color(r, g, b) = red;
println!("Red channel: {r}");

// Newtype pattern — single-field tuple struct for type safety
struct Meters(f64);
struct Seconds(f64);

fn speed(distance: Meters, time: Seconds) -> f64 {
    distance.0 / time.0
}
// speed(Seconds(5.0), Meters(100.0));  // ERROR: wrong argument order!
let s = speed(Meters(100.0), Seconds(5.0));  // correct

// Unit structs — no fields, zero size
struct Marker;
struct AlwaysEqual;

let _m = Marker;
let _e = AlwaysEqual;
// Useful for implementing traits on types that don't hold data

impl Blocks: Methods & Associated Functions

Use impl blocks to define methods (called on instances) and associated functions (called on the type itself). The first parameter of a method determines how it accesses the instance.

First Parameter Access Level Call Syntax Use When
&self Immutable borrow instance.method() Reading fields, computing values
&mut self Mutable borrow instance.method() Modifying the instance
self Takes ownership instance.method() Consuming / transforming the instance
no self Associated function Type::function() Constructors, utility functions
struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    // Associated function (constructor) — no self parameter
    fn new(width: f64, height: f64) -> Self {
        Self { width, height }       // Self refers to the impl type (Rectangle)
    }

    fn square(size: f64) -> Self {
        Self { width: size, height: size }
    }

    // Method with &self — immutable borrow (read-only)
    fn area(&self) -> f64 {
        self.width * self.height
    }

    fn perimeter(&self) -> f64 {
        2.0 * (self.width + self.height)
    }

    fn is_square(&self) -> bool {
        (self.width - self.height).abs() < f64::EPSILON
    }

    // Method with &mut self — mutable borrow (modifies in place)
    fn scale(&mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }

    // Method with self — takes ownership (consumes the instance)
    fn into_square(self) -> Rectangle {
        let side = self.width.max(self.height);
        Rectangle { width: side, height: side }
    }
}

// Usage
let r = Rectangle::new(10.0, 5.0);   // associated function with ::
println!("Area: {}", r.area());        // method with .
println!("Square? {}", r.is_square());

let mut r = Rectangle::new(3.0, 4.0);
r.scale(2.0);                          // mutates r
println!("Scaled area: {}", r.area()); // 48.0

let sq = r.into_square();              // consumes r
// println!("{}", r.area());           // ERROR: r was moved
println!("Square area: {}", sq.area());

Deriving Traits

The #[derive] attribute auto-generates trait implementations. This is the idiomatic way to add common functionality to your types.

// Common derivable traits
#[derive(Debug, Clone, PartialEq)]
struct Product {
    name: String,
    price: f64,
    in_stock: bool,
}

let p1 = Product {
    name: "Widget".into(),
    price: 9.99,
    in_stock: true,
};

// Debug — enables {:?} formatting
println!("{:?}", p1);
// Product { name: "Widget", price: 9.99, in_stock: true }

println!("{:#?}", p1);               // pretty-printed Debug output

// Clone — explicit deep copy
let p2 = p1.clone();

// PartialEq — enables == and != comparisons
assert_eq!(p1, p2);                   // true — same values
Derive Provides Requirement
Debug {:?} formatting All fields implement Debug
Clone .clone() method All fields implement Clone
Copy Implicit bitwise copy All fields implement Copy + Clone
PartialEq == and != All fields implement PartialEq
Eq Full equality (reflexive) Requires PartialEq, no f32/f64 fields
PartialOrd <, >, <=, >= All fields implement PartialOrd
Ord Total ordering Requires Eq + PartialOrd
Hash Hashing for HashMap keys All fields implement Hash
Default Type::default() All fields implement Default
// Multiple derives on one type
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
struct Config {
    name: String,
    retries: u32,
    verbose: bool,
}

let default_config = Config::default();
// Config { name: "", retries: 0, verbose: false }

// Derive Copy for stack-only types
#[derive(Debug, Copy, Clone, PartialEq)]
struct Vector2D {
    x: f64,
    y: f64,
}

let v1 = Vector2D { x: 1.0, y: 2.0 };
let v2 = v1;                         // implicit copy, v1 still valid
println!("{:?} and {:?}", v1, v2);

Enums with Data

Rust enums are far more powerful than C-style enums. Each variant can hold different types and amounts of data, making them ideal for modeling states and alternatives.

// Basic enum (C-style, no data)
enum Direction {
    North,
    South,
    East,
    West,
}

let heading = Direction::North;

// Enum variants can hold data of different types
enum Message {
    Quit,                              // no data (unit variant)
    Echo(String),                      // single value (tuple variant)
    Move { x: i32, y: i32 },          // named fields (struct variant)
    Color(u8, u8, u8),                 // multiple values (tuple variant)
}

let msg1 = Message::Quit;
let msg2 = Message::Echo(String::from("hello"));
let msg3 = Message::Move { x: 10, y: 20 };
let msg4 = Message::Color(255, 128, 0);

// Enums can have methods too
impl Message {
    fn is_quit(&self) -> bool {
        matches!(self, Message::Quit)
    }

    fn process(&self) {
        match self {
            Message::Quit => println!("Quitting"),
            Message::Echo(text) => println!("Echo: {text}"),
            Message::Move { x, y } => println!("Moving to ({x}, {y})"),
            Message::Color(r, g, b) => println!("Color: rgb({r}, {g}, {b})"),
        }
    }
}

msg3.process();                        // "Moving to (10, 20)"
// Real-world enum: modeling API responses
#[derive(Debug)]
enum ApiResponse {
    Success { data: String, status: u16 },
    Error { message: String, code: u16 },
    Loading,
    NotFound,
}

// Real-world enum: representing different shapes
#[derive(Debug)]
enum Shape {
    Circle(f64),                       // radius
    Rectangle(f64, f64),               // width, height
    Triangle { base: f64, height: f64 },
}

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle(r) => std::f64::consts::PI * r * r,
            Shape::Rectangle(w, h) => w * h,
            Shape::Triangle { base, height } => 0.5 * base * height,
        }
    }
}

let shapes = vec![
    Shape::Circle(5.0),
    Shape::Rectangle(4.0, 6.0),
    Shape::Triangle { base: 3.0, height: 8.0 },
];

for shape in &shapes {
    println!("{:?} has area {:.2}", shape, shape.area());
}

Option<T> in Depth

Option<T> replaces null in Rust. A value is either Some(T) (present) or None (absent). The compiler forces you to handle both cases, eliminating null pointer exceptions.

// Option is defined as:
// enum Option<T> {
//     Some(T),
//     None,
// }

// Creating Options
let some_number: Option<i32> = Some(42);
let no_number: Option<i32> = None;

// From a function that might not find a result
fn find_user(id: u32) -> Option<String> {
    if id == 1 {
        Some(String::from("Alice"))
    } else {
        None
    }
}

let user = find_user(1);              // Some("Alice")
let missing = find_user(99);          // None
// Common Option methods

let x: Option<i32> = Some(42);
let y: Option<i32> = None;

// unwrap — get the value or panic
let val = x.unwrap();                 // 42
// y.unwrap();                        // PANIC: called unwrap on None

// expect — unwrap with a custom panic message
let val = x.expect("should have a value");

// unwrap_or — provide a default
let val = y.unwrap_or(0);             // 0

// unwrap_or_else — compute default lazily
let val = y.unwrap_or_else(|| compute_default());

// unwrap_or_default — use the Default trait
let val: i32 = y.unwrap_or_default(); // 0 (i32's default)

// is_some / is_none — check without consuming
if x.is_some() {
    println!("x has a value");
}

// map — transform the inner value
let doubled = x.map(|v| v * 2);       // Some(84)
let doubled = y.map(|v| v * 2);       // None (no-op)

// and_then (flatmap) — chain operations that return Option
fn parse_and_double(s: &str) -> Option<i32> {
    s.parse::<i32>().ok()              // Result -> Option
        .and_then(|n| n.checked_mul(2)) // returns Option
}
let result = parse_and_double("21");   // Some(42)
let result = parse_and_double("abc");  // None

// filter — keep Some only if predicate matches
let even = Some(4).filter(|x| x % 2 == 0);  // Some(4)
let odd = Some(3).filter(|x| x % 2 == 0);   // None

// zip — combine two Options
let name = Some("Alice");
let age = Some(30);
let pair = name.zip(age);             // Some(("Alice", 30))

Avoid Excessive unwrap()

Using unwrap() in production code is a code smell because it panics on None. Prefer match, if let, unwrap_or(), or the ? operator. Reserve unwrap() for tests, examples, and cases where you can prove the value is always Some.

Result<T, E> in Depth

Result<T, E> is Rust's primary error-handling type. Operations that can fail return Ok(T) on success or Err(E) on failure. The compiler ensures you handle both cases.

// Result is defined as:
// enum Result<T, E> {
//     Ok(T),
//     Err(E),
// }

// Functions that can fail return Result
use std::fs;
use std::num::ParseIntError;

fn parse_number(s: &str) -> Result<i32, ParseIntError> {
    s.parse::<i32>()
}

let good = parse_number("42");        // Ok(42)
let bad = parse_number("abc");        // Err(ParseIntError { ... })
// Common Result methods

let ok: Result<i32, String> = Ok(42);
let err: Result<i32, String> = Err("oops".into());

// unwrap / expect — get value or panic on Err
let val = ok.unwrap();                 // 42
let val = ok.expect("parsing failed"); // 42
// err.unwrap();                       // PANIC with error message

// unwrap_or / unwrap_or_else / unwrap_or_default
let val = err.unwrap_or(0);            // 0
let val = err.unwrap_or_else(|e| {
    eprintln!("Error: {e}");
    -1
});

// is_ok / is_err
if ok.is_ok() {
    println!("Success!");
}

// map — transform the success value
let doubled = ok.map(|v| v * 2);       // Ok(84)

// map_err — transform the error value
let mapped = err.map_err(|e| format!("Error: {e}"));

// and_then — chain fallible operations
fn double_parse(s: &str) -> Result<i32, ParseIntError> {
    s.parse::<i32>()
        .and_then(|n| Ok(n * 2))
}

// ok() — convert Result to Option (discards error)
let opt: Option<i32> = ok.ok();        // Some(42)

// The ? operator — propagate errors (most idiomatic)
fn read_and_parse(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let contents = fs::read_to_string(path)?;  // returns Err early if fails
    let number = contents.trim().parse::<i32>()?; // same here
    Ok(number * 2)
}
// The ? operator:
//   1. If Ok(v), unwraps v and continues
//   2. If Err(e), converts e (via From) and returns early

Result vs Option

Option<T> means "value or nothing" — use it when absence is not an error (e.g., finding an element). Result<T, E> means "value or error" — use it when failure needs an explanation (e.g., file I/O, parsing). Convert between them with .ok() (Result to Option) and .ok_or(err) (Option to Result).

When to Use Struct vs Enum

Use a Struct When

You have a single concept with multiple attributes that are ALL present at once.

// All users have all fields
struct User {
    name: String,
    email: String,
    age: u32,
}

// A point always has x and y
struct Point {
    x: f64,
    y: f64,
}

// Configuration with many settings
struct Config {
    host: String,
    port: u16,
    debug: bool,
}

Use an Enum When

A value can be ONE of several alternatives, each potentially with different data.

// A shape is one specific kind
enum Shape {
    Circle(f64),
    Rectangle(f64, f64),
}

// A message is one specific type
enum Command {
    Start,
    Stop,
    Resize { w: u32, h: u32 },
}

// State machines
enum ConnectionState {
    Disconnected,
    Connecting(String),
    Connected { addr: String },
}
// Often you combine both: structs inside enums
#[derive(Debug)]
struct Credentials {
    username: String,
    token: String,
}

#[derive(Debug)]
enum AuthState {
    Anonymous,
    Authenticated(Credentials),       // enum variant holds a struct
    Banned { reason: String, until: String },
}

let state = AuthState::Authenticated(Credentials {
    username: "ferris".into(),
    token: "abc123".into(),
});
println!("{state:?}");

When to Use Option vs Result

Use Option<T> When

A value might or might not exist, and the absence is not an error. The caller does not need to know why the value is missing.

// Finding an element
fn find_user(id: u64) -> Option<User>

// Getting the first element
vec.first()  // -> Option<&T>

// Parsing optional fields
struct Config {
    name: String,
    description: Option<String>,
    max_retries: Option<u32>,
}

// HashMap lookup
map.get(&key)  // -> Option<&V>

Use Result<T, E> When

An operation can fail, and the caller needs to know what went wrong. The error carries diagnostic information.

// File operations
fn read_file(p: &str) -> Result<String, io::Error>

// Parsing with error info
fn parse_config(s: &str) -> Result<Config, ParseError>

// Network requests
fn fetch(url: &str) -> Result<Response, HttpError>

// Validation
fn validate(input: &str) -> Result<(), ValidationError>

// Database queries
fn query(sql: &str) -> Result<Rows, DbError>

Rule of Thumb

If "nothing found" is a normal, expected outcome, use Option. If "nothing found" means something went wrong and the caller might need to react differently based on the cause, use Result. When in doubt, prefer Result — you can always convert it to an Option with .ok(), but going the other way loses error information.

06

Pattern Matching

Pattern matching is one of Rust's most expressive features. The match expression, if let, while let, and destructuring let you elegantly handle complex data shapes.

match Expressions

A match expression compares a value against a series of patterns and executes the code for the first matching pattern. Matching is exhaustive — the compiler ensures every possible value is covered.

// Basic match — must cover ALL possible values
let number = 3;
let text = match number {
    1 => "one",
    2 => "two",
    3 => "three",
    _ => "other",                     // _ is the catch-all (wildcard) pattern
};
println!("{text}");                    // "three"

// match is an expression — it returns a value
let x = 42;
let description = match x {
    0 => "zero",
    1..=9 => "single digit",          // inclusive range pattern
    10..=99 => "double digit",
    100..=999 => "triple digit",
    _ => "big number",
};

// Multiple patterns with |  (or-pattern)
let c = 'y';
let is_yes = match c {
    'y' | 'Y' => true,
    'n' | 'N' => false,
    _ => {
        println!("Unknown response");
        false
    }
};

// match with blocks (use {} for multi-line arms)
let temp = 35;
match temp {
    t if t < 0 => {
        println!("Freezing: {t}C");
        println!("Stay inside!");
    }
    0..=15 => println!("Cold"),
    16..=25 => println!("Comfortable"),
    26..=35 => println!("Warm"),
    _ => println!("Hot!"),
}

Exhaustive Matching

The Rust compiler requires that match handles every possible value. For integers and strings, use _ as a catch-all. For enums, you must match every variant (or use _). This prevents bugs from unhandled cases and ensures your code adapts when new variants are added.

Matching on Enums

Pattern matching truly shines with enums, especially Option and Result. You can destructure variant data directly in the match arms.

// Matching Option<T>
fn describe_number(n: Option<i32>) -> String {
    match n {
        Some(0) => "zero".to_string(),
        Some(x) if x > 0 => format!("positive: {x}"),
        Some(x) => format!("negative: {x}"),
        None => "no number".to_string(),
    }
}

println!("{}", describe_number(Some(42)));   // "positive: 42"
println!("{}", describe_number(None));        // "no number"

// Matching Result<T, E>
use std::num::ParseIntError;

fn try_parse(input: &str) -> String {
    let result: Result<i32, ParseIntError> = input.parse();
    match result {
        Ok(n) if n >= 0 => format!("Got positive number: {n}"),
        Ok(n) => format!("Got negative number: {n}"),
        Err(e) => format!("Parse error: {e}"),
    }
}

// Matching custom enums
#[derive(Debug)]
enum Command {
    Quit,
    Echo(String),
    Move { x: i32, y: i32 },
    Color(u8, u8, u8),
}

fn execute(cmd: &Command) {
    match cmd {
        Command::Quit => {
            println!("Exiting");
            std::process::exit(0);
        }
        Command::Echo(msg) => println!("{msg}"),
        Command::Move { x, y } => println!("Moving to ({x}, {y})"),
        Command::Color(r, g, b) => println!("Setting color #{r:02x}{g:02x}{b:02x}"),
    }
}

let cmd = Command::Color(255, 128, 0);
execute(&cmd);                         // "Setting color #ff8000"

Destructuring Structs & Tuples in match

Patterns can destructure structs, tuples, and nested types, letting you extract exactly the data you need.

// Destructuring tuples
let point = (3, -5);
match point {
    (0, 0) => println!("Origin"),
    (x, 0) => println!("On x-axis at {x}"),
    (0, y) => println!("On y-axis at {y}"),
    (x, y) if x > 0 && y > 0 => println!("Quadrant I: ({x}, {y})"),
    (x, y) => println!("Somewhere else: ({x}, {y})"),
}

// Destructuring structs
struct Point {
    x: i32,
    y: i32,
}

let p = Point { x: 0, y: 7 };
match p {
    Point { x: 0, y } => println!("On y-axis at y={y}"),
    Point { x, y: 0 } => println!("On x-axis at x={x}"),
    Point { x, y } => println!("Point at ({x}, {y})"),
}

// Ignoring fields with ..
struct Config {
    host: String,
    port: u16,
    debug: bool,
    retries: u32,
}

let cfg = Config {
    host: "localhost".into(),
    port: 8080,
    debug: true,
    retries: 3,
};

match &cfg {
    Config { debug: true, .. } => println!("Debug mode enabled"),
    Config { port: 443, .. } => println!("HTTPS detected"),
    _ => println!("Standard config"),
}

// Nested destructuring
enum Wrapper {
    Pair(Point, Point),
    Single(Point),
    Empty,
}

let w = Wrapper::Pair(
    Point { x: 1, y: 2 },
    Point { x: 3, y: 4 },
);

match w {
    Wrapper::Pair(Point { x: x1, y: y1 }, Point { x: x2, y: y2 }) => {
        println!("Pair: ({x1},{y1}) to ({x2},{y2})");
    }
    Wrapper::Single(Point { x, y }) => println!("Single: ({x},{y})"),
    Wrapper::Empty => println!("Empty"),
}

if let

When you only care about one pattern and want to ignore the rest, if let is a concise alternative to a full match with a wildcard arm.

// Instead of this verbose match:
let config_max: Option<u32> = Some(3);
match config_max {
    Some(max) => println!("Max is {max}"),
    _ => (),                          // do nothing for None
}

// Use if let:
if let Some(max) = config_max {
    println!("Max is {max}");
}

// if let with else
let value: Option<i32> = None;
if let Some(v) = value {
    println!("Found: {v}");
} else {
    println!("Nothing found");
}

// if let with enum variants
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(String),                   // state name
}

let coin = Coin::Quarter("Alaska".to_string());
if let Coin::Quarter(state) = &coin {
    println!("State quarter from {state}");
}

// Chaining if let for multiple checks
fn process_input(input: &str) {
    if let Ok(number) = input.parse::<i32>() {
        println!("Got number: {number}");
    } else if let Ok(float) = input.parse::<f64>() {
        println!("Got float: {float}");
    } else {
        println!("Got text: {input}");
    }
}

process_input("42");                   // "Got number: 42"
process_input("3.14");                 // "Got float: 3.14"
process_input("hello");               // "Got text: hello"

while let

while let loops as long as a pattern matches. It is commonly used to consume iterators or drain collections that return Option.

// Pop elements from a vector until empty
let mut stack = vec![1, 2, 3, 4, 5];

while let Some(top) = stack.pop() {
    println!("Popped: {top}");
}
// Output: 5, 4, 3, 2, 1 (pop removes from the end)
println!("Stack is now empty: {:?}", stack);

// Iterating over an iterator
let names = vec!["Alice", "Bob", "Charlie"];
let mut iter = names.iter();

while let Some(name) = iter.next() {
    println!("Hello, {name}!");
}
// Note: a `for` loop is usually preferred for iterators,
// but while let is useful when you need custom control flow

// Processing a channel receiver
// use std::sync::mpsc;
// let (tx, rx) = mpsc::channel();
// while let Ok(message) = rx.recv() {
//     println!("Got: {message}");
// }

// Parsing optional nested structures
fn next_token(input: &mut &str) -> Option<char> {
    let ch = input.chars().next()?;
    *input = &input[ch.len_utf8()..];
    Some(ch)
}

let mut text = "Rust";
while let Some(ch) = next_token(&mut text) {
    print!("{ch} ");
}
// Output: R u s t

let-else (Early Returns)

The let-else syntax (stabilized in Rust 1.65) lets you bind a pattern or diverge (return, break, continue, panic). It is the idiomatic way to do "unwrap or return early."

// let-else: bind the pattern or execute the else block (which must diverge)
fn get_username(id: Option<u32>) -> String {
    let Some(id) = id else {
        return "anonymous".to_string();  // must return, break, continue, or panic
    };
    format!("user_{id}")
}

println!("{}", get_username(Some(42)));    // "user_42"
println!("{}", get_username(None));         // "anonymous"

// let-else with Result
fn parse_port(s: &str) -> Result<u16, String> {
    let Ok(port) = s.parse::<u16>() else {
        return Err(format!("Invalid port: {s}"));
    };

    if port == 0 {
        return Err("Port 0 is reserved".into());
    }

    Ok(port)
}

// let-else is great for reducing nesting
// Before (nested):
fn process_v1(input: &str) -> Option<i32> {
    if let Ok(n) = input.parse::<i32>() {
        if n > 0 {
            Some(n * 2)
        } else {
            None
        }
    } else {
        None
    }
}

// After (flat with let-else):
fn process_v2(input: &str) -> Option<i32> {
    let Ok(n) = input.parse::<i32>() else {
        return None;
    };
    if n <= 0 {
        return None;
    }
    Some(n * 2)
}

// let-else with complex patterns
struct Request {
    method: String,
    path: String,
    body: Option<String>,
}

fn handle_post(req: &Request) -> Result<String, String> {
    let Some(body) = &req.body else {
        return Err("POST requires a body".into());
    };
    Ok(format!("Processing: {body}"))
}

let-else vs if let

if let is for "do something when a pattern matches." let-else is for "bind the pattern or bail out." Use let-else when the non-matching case should exit the function, continue a loop, or otherwise diverge. It keeps the "happy path" un-nested and easy to follow.

Match Guards

A match guard is an additional if condition on a match arm. It refines the pattern with arbitrary boolean expressions.

// Match guard with if
let num = Some(4);
match num {
    Some(x) if x < 0 => println!("Negative: {x}"),
    Some(x) if x == 0 => println!("Zero"),
    Some(x) if x % 2 == 0 => println!("Positive even: {x}"),
    Some(x) => println!("Positive odd: {x}"),
    None => println!("Nothing"),
}
// Output: "Positive even: 4"

// Guards can reference outer variables
let target = 5;
let numbers = [1, 5, 10, 15, 5, 20];

for &n in &numbers {
    match n {
        x if x == target => println!("Found target: {x}"),
        x if x > target => println!("{x} is above target"),
        x => println!("{x} is below target"),
    }
}

// Guards with or-patterns — the guard applies to ALL alternatives
let x = 4;
let y = false;
match x {
    4 | 5 | 6 if y => println!("4, 5, or 6 and y is true"),
    4 | 5 | 6 => println!("4, 5, or 6 (y is false)"),
    _ => println!("something else"),
}
// Output: "4, 5, or 6 (y is false)"
// Note: the guard `if y` applies to ALL of 4 | 5 | 6, not just 6

// Guards in real-world code
enum Temperature {
    Celsius(f64),
    Fahrenheit(f64),
}

fn describe_temp(temp: &Temperature) -> &str {
    match temp {
        Temperature::Celsius(c) if *c < 0.0 => "freezing",
        Temperature::Celsius(c) if *c > 35.0 => "hot",
        Temperature::Celsius(_) => "moderate",
        Temperature::Fahrenheit(f) if *f < 32.0 => "freezing",
        Temperature::Fahrenheit(f) if *f > 95.0 => "hot",
        Temperature::Fahrenheit(_) => "moderate",
    }
}

@ Bindings

The @ operator lets you bind a value to a name while simultaneously testing it against a pattern. This is useful when you need both the whole value and a pattern constraint.

// Bind a name to a matched range value
let age = 25;
match age {
    child @ 0..=12 => println!("Child, age {child}"),
    teen @ 13..=17 => println!("Teenager, age {teen}"),
    adult @ 18..=64 => println!("Adult, age {adult}"),
    senior @ 65.. => println!("Senior, age {senior}"),
    // Note: patterns starting from a value use 65.. (no end bound)
}
// Output: "Adult, age 25"

// @ with enum variants
enum Message {
    Hello { id: i32 },
    Goodbye,
}

let msg = Message::Hello { id: 5 };
match msg {
    Message::Hello { id: id_val @ 3..=7 } => {
        println!("Found id in range 3-7: {id_val}");
    }
    Message::Hello { id } => {
        println!("Other id: {id}");
    }
    Message::Goodbye => println!("Goodbye"),
}
// Output: "Found id in range 3-7: 5"

// @ with Option to capture the whole Some value
let numbers = vec![Some(3), None, Some(7), Some(1), None];

for num in &numbers {
    match num {
        some_val @ Some(x) if *x > 5 => {
            println!("Large value: {some_val:?} contains {x}");
        }
        Some(x) => println!("Small value: {x}"),
        None => println!("Missing"),
    }
}

// @ in nested patterns
struct Point { x: i32, y: i32 }

let p = Point { x: 5, y: 10 };
match p {
    Point { x: x_val @ 0..=10, y: y_val @ 0..=10 } => {
        println!("In bounds: ({x_val}, {y_val})");
    }
    Point { x, y } => println!("Out of bounds: ({x}, {y})"),
}

Nested Patterns & Or-Patterns

Patterns can be nested to match complex data structures, and or-patterns (|) let you match multiple alternatives in a single arm.

// Or-patterns with | — match any of several values
let status_code = 404;
let category = match status_code {
    200 | 201 | 202 => "Success",
    301 | 302 => "Redirect",
    400 | 401 | 403 => "Client Error",
    404 => "Not Found",
    500 | 502 | 503 => "Server Error",
    _ => "Unknown",
};

// Or-patterns with enum variants
#[derive(Debug)]
enum Animal {
    Dog(String),
    Cat(String),
    Fish,
    Bird(String),
}

let pet = Animal::Cat("Whiskers".to_string());
match &pet {
    Animal::Dog(name) | Animal::Cat(name) => {
        println!("Furry pet named {name}");
    }
    Animal::Bird(name) => println!("Feathered friend: {name}"),
    Animal::Fish => println!("Swimming silently"),
}

// Nested patterns — matching deep structures
#[derive(Debug)]
enum Expr {
    Num(f64),
    Add(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
    Neg(Box<Expr>),
}

fn simplify(expr: &Expr) -> String {
    match expr {
        Expr::Num(n) => format!("{n}"),
        Expr::Add(box Expr::Num(0.0), b) |
        Expr::Add(b, box Expr::Num(0.0)) => {
            // Note: box patterns are nightly-only as of early 2025
            // In stable Rust, use nested match or if let instead
            simplify(b)
        }
        Expr::Add(a, b) => format!("({} + {})", simplify(a), simplify(b)),
        Expr::Mul(a, b) => format!("({} * {})", simplify(a), simplify(b)),
        Expr::Neg(inner) => format!("-{}", simplify(inner)),
    }
}

// Stable alternative for nested matching — multi-level match
fn eval(expr: &Expr) -> f64 {
    match expr {
        Expr::Num(n) => *n,
        Expr::Add(a, b) => eval(a) + eval(b),
        Expr::Mul(a, b) => eval(a) * eval(b),
        Expr::Neg(inner) => -eval(inner),
    }
}
// Complex real-world pattern matching example
#[derive(Debug)]
struct HttpRequest {
    method: String,
    path: String,
    auth: Option<String>,
}

fn route(req: &HttpRequest) -> String {
    match (req.method.as_str(), req.path.as_str(), &req.auth) {
        ("GET", "/", _) => "Home page".into(),
        ("GET", "/api/users", Some(_)) => "User list (authenticated)".into(),
        ("GET", "/api/users", None) => "401 Unauthorized".into(),
        ("POST", "/api/users", Some(token)) => {
            format!("Creating user with token: {token}")
        }
        ("DELETE", path, Some(_)) if path.starts_with("/api/") => {
            format!("Deleting resource: {path}")
        }
        (method, path, _) => format!("404: {method} {path} not found"),
    }
}

let req = HttpRequest {
    method: "GET".into(),
    path: "/api/users".into(),
    auth: Some("bearer_token_123".into()),
};
println!("{}", route(&req));
// "User list (authenticated)"

Pattern Matching Best Practices

Prefer match when you have 3+ cases or need exhaustive coverage. Use if let for single-pattern checks. Use let-else for "unwrap or bail." Use matches!(value, pattern) when you only need a boolean. Avoid deeply nested patterns — extract helper functions for readability.

Patterns in Function Parameters

Function parameters are patterns too. You can destructure tuples, structs, and other types directly in the function signature and in closures.

// Destructure a tuple in parameters
fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("({x}, {y})");
}

let point = (3, 5);
print_coordinates(&point);

// Destructure a struct in parameters
fn distance_from_origin(&Point { x, y }: &Point) -> f64 {
    ((x * x + y * y) as f64).sqrt()
}

// Destructure in closures
let points = vec![(1, 2), (3, 4), (5, 6)];
let sum: i32 = points.iter()
    .map(|(x, y)| x + y)       // destructure tuple in closure parameter
    .sum();
println!("Sum: {sum}");

// Ignoring values with _
fn log_result(result: Result<i32, String>) {
    match result {
        Ok(_) => println!("success (value ignored)"),
        Err(_) => println!("failure (error ignored)"),
    }
}

// Ignoring parts of a struct with ..
let Point { x, .. } = Point { x: 10, y: 20 };

// Ignoring remaining tuple elements
let (first, ..) = (1, 2, 3, 4, 5);
let (.., last) = (1, 2, 3, 4, 5);
let (first, .., last) = (1, 2, 3, 4, 5);
println!("{first} ... {last}");         // 1 ... 5

Matching on References

When matching on references, you need to understand how patterns interact with & and the ref keyword to control whether you dereference or borrow.

// Matching a reference — use & in the pattern to "look through" it
let reference = &4;

match reference {
    &val => println!("Got a value: {val}"),   // val is i32, not &i32
}

// Equivalent using dereferencing
match *reference {
    val => println!("Got a value: {val}"),
}

// The ref keyword — create a reference in a pattern binding
let value = 5;

match value {
    ref r => println!("Got a reference: {r}"),  // r is &i32
}
// ref is rarely needed in modern Rust because match ergonomics handle it

// Match ergonomics (Rust 2018+) — auto-referencing in patterns
let opt: &Option<String> = &Some(String::from("hello"));

// Before match ergonomics, you had to write:
// match opt { &Some(ref s) => ..., &None => ... }

// Now you can write (compiler auto-inserts & and ref):
match opt {
    Some(s) => println!("{s}"),              // s is &String (auto-borrowed)
    None => println!("nothing"),
}

// Matching in for loops (iterating over references)
let names = vec!["Alice", "Bob", "Charlie"];

for name in &names {
    // name is &&str here; patterns handle it naturally
    match name {
        &"Alice" => println!("Found Alice!"),
        other => println!("Hello, {other}"),
    }
}

// Common pattern: matching Option<&T> vs &Option<T>
let v = vec![1, 2, 3];
match v.first() {                            // returns Option<&i32>
    Some(&1) => println!("starts with one"),
    Some(n) => println!("starts with {n}"),  // n is &i32
    None => println!("empty"),
}

Pattern Matching Cheat Sheet

Use match when you need exhaustive handling of all cases. Use if let when you care about one case. Use let-else when you want to early-return on a mismatch. Use while let to loop until a pattern stops matching. In modern Rust, match ergonomics handle most reference situations automatically — start simple and only add & or ref if the compiler asks for it.

07

Functions & Closures

Functions are the primary unit of code reuse in Rust. Closures add anonymous, environment-capturing function expressions. Together with iterators, they form the backbone of idiomatic Rust.

Function Syntax

Functions are declared with fn. Every parameter must have an explicit type annotation. The return type follows ->. Rust is expression-based: the last expression in a function body (without a trailing semicolon) is the return value.

// Basic function with parameters and return type
fn add(a: i32, b: i32) -> i32 {
    a + b                              // last expression = implicit return (no semicolon)
}

// No return value (returns unit type `()`)
fn greet(name: &str) {
    println!("Hello, {name}!");
}

// Explicit early return with `return` keyword
fn first_positive(numbers: &[i32]) -> Option<i32> {
    for &n in numbers {
        if n > 0 {
            return Some(n);            // early return
        }
    }
    None                               // implicit return
}

// Multiple return values via tuples
fn min_max(values: &[i32]) -> (i32, i32) {
    let mut min = values[0];
    let mut max = values[0];
    for &v in &values[1..] {
        if v < min { min = v; }
        if v > max { max = v; }
    }
    (min, max)
}

let (lo, hi) = min_max(&[3, 1, 4, 1, 5, 9]);
println!("min={lo}, max={hi}");        // min=1, max=9

// Expression-based returns in blocks
let result = {
    let x = 10;
    let y = 20;
    x + y                              // block evaluates to 30
};
println!("{result}");                  // 30

// if/else as expressions
fn abs(n: i32) -> i32 {
    if n < 0 { -n } else { n }
}

// Never type (!) for functions that never return
fn forever() -> ! {
    loop {
        // runs indefinitely
    }
}

Generic Functions & Where Clauses

Generics let you write functions that work across multiple types. Type parameters go in angle brackets after the function name. Trait bounds constrain what the type must support. For complex bounds, use where clauses for readability.

// Generic function with a trait bound
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut max = &list[0];
    for item in &list[1..] {
        if item > max {
            max = item;
        }
    }
    max
}

println!("{}", largest(&[34, 50, 25, 100, 65]));   // 100
println!("{}", largest(&["apple", "banana", "cherry"])); // cherry

// Multiple trait bounds with +
fn print_and_clone<T: std::fmt::Display + Clone>(item: &T) -> T {
    println!("Value: {item}");
    item.clone()
}

// Where clause for complex bounds (preferred for readability)
fn process<T, U>(t: &T, u: &U) -> String
where
    T: std::fmt::Display + Clone,
    U: std::fmt::Debug + PartialEq,
{
    format!("t={t}, u={u:?}")
}

// Const generics (arrays of specific sizes)
fn first_n<const N: usize>(slice: &[i32]) -> [i32; N] {
    let mut arr = [0i32; N];
    arr.copy_from_slice(&slice[..N]);
    arr
}

let first_three: [i32; 3] = first_n(&[10, 20, 30, 40, 50]);
println!("{:?}", first_three);         // [10, 20, 30]

// Returning impl Trait (hide concrete type)
fn make_greeting(name: &str) -> impl std::fmt::Display {
    format!("Hello, {name}!")
}

Closures

Closures are anonymous functions that can capture variables from their enclosing scope. The syntax uses |args| for parameters. Rust infers parameter and return types for closures (though you can annotate them explicitly). How the closure captures its environment determines which Fn trait it implements.

// Basic closure syntax
let add = |a, b| a + b;
println!("{}", add(3, 4));             // 7

// Closure with type annotations (optional)
let multiply: fn(i32, i32) -> i32 = |a: i32, b: i32| -> i32 { a * b };

// Multi-line closure with braces
let process = |name: &str| {
    let greeting = format!("Hello, {name}!");
    println!("{greeting}");
    greeting.len()
};

// Capturing variables from the environment
let base = 10;
let add_base = |x| x + base;          // captures `base` by reference (&)
println!("{}", add_base(5));           // 15

// Mutable capture (&mut)
let mut count = 0;
let mut increment = || {
    count += 1;                        // captures `count` by &mut
    count
};
println!("{}", increment());           // 1
println!("{}", increment());           // 2

// Move capture (takes ownership)
let name = String::from("Rust");
let greet = move || {
    println!("Hello from {name}!");    // `name` is moved into the closure
};
greet();
// println!("{name}");                 // ERROR: name was moved

Capture Modes

Rust automatically picks the least restrictive capture mode: & (shared reference) if possible, then &mut (mutable reference), then move (ownership). Use the move keyword before the closure to force all captures to take ownership — essential when sending closures to other threads or returning them from functions.

Fn, FnMut, and FnOnce Traits

Every closure implements one or more of the three Fn traits. The trait determines how the closure can be called and constrains where you can use it.

Trait Capture Can Call Use Case
Fn(&self) Borrows by shared reference & Multiple times Read-only access to captured variables; most closures
FnMut(&mut self) Borrows by mutable reference &mut Multiple times Mutates captured variables (e.g., counters, accumulators)
FnOnce(self) Takes ownership (moves captures) Exactly once Consumes captured values (e.g., returning an owned value)
// Fn: borrows immutably, can be called many times
fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
    f(f(x))
}
let double = |x| x * 2;
println!("{}", apply_twice(double, 3));  // 12

// FnMut: borrows mutably
fn call_n_times<F: FnMut()>(mut f: F, n: usize) {
    for _ in 0..n {
        f();
    }
}
let mut total = 0;
call_n_times(|| total += 1, 5);
println!("{total}");                   // 5

// FnOnce: consumes captured values, callable only once
fn consume<F: FnOnce() -> String>(f: F) -> String {
    f()
}
let name = String::from("Rust");
let greeting = consume(move || format!("Hello, {name}!"));
println!("{greeting}");                // Hello, Rust!

// Returning closures (must use impl Fn or Box<dyn Fn>)
fn make_adder(base: i32) -> impl Fn(i32) -> i32 {
    move |x| x + base
}
let add_ten = make_adder(10);
println!("{}", add_ten(5));            // 15

// Box<dyn Fn> when returning different closures conditionally
fn choose_op(add: bool) -> Box<dyn Fn(i32, i32) -> i32> {
    if add {
        Box::new(|a, b| a + b)
    } else {
        Box::new(|a, b| a * b)
    }
}
let op = choose_op(true);
println!("{}", op(3, 4));              // 7

Iterator Methods with Closures

Rust's iterator trait provides a rich set of combinators that accept closures. These are zero-cost abstractions — the compiler optimizes them into tight loops equivalent to hand-written code.

let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// map: transform each element
let doubled: Vec<i32> = numbers.iter().map(|&x| x * 2).collect();
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

// filter: keep elements matching a predicate
let evens: Vec<&i32> = numbers.iter().filter(|&&x| x % 2 == 0).collect();
// [2, 4, 6, 8, 10]

// fold: reduce to a single value (like reduce in other languages)
let sum = numbers.iter().fold(0, |acc, &x| acc + x);
// 55

// for_each: side effects for each element (no collecting)
numbers.iter().for_each(|x| print!("{x} "));
// 1 2 3 4 5 6 7 8 9 10

// find: first element matching a predicate (returns Option)
let first_big = numbers.iter().find(|&&x| x > 7);
// Some(8)

// any: does any element match?
let has_negative = numbers.iter().any(|&x| x < 0);
// false

// all: do all elements match?
let all_positive = numbers.iter().all(|&x| x > 0);
// true

// collect into different types
let as_strings: Vec<String> = numbers.iter().map(|x| x.to_string()).collect();
let as_set: std::collections::HashSet<&i32> = numbers.iter().collect();

// Chaining: filter, transform, and collect in one pipeline
let result: Vec<String> = numbers.iter()
    .filter(|&&x| x % 2 == 0)
    .map(|x| format!("{x} is even"))
    .collect();
// ["2 is even", "4 is even", "6 is even", "8 is even", "10 is even"]

Iterator Adaptors

Beyond map and filter, the standard library provides powerful adaptors for composing complex iteration pipelines.

let a = vec![1, 2, 3];
let b = vec![4, 5, 6];

// chain: concatenate two iterators
let chained: Vec<i32> = a.iter().chain(b.iter()).copied().collect();
// [1, 2, 3, 4, 5, 6]

// zip: pair up elements from two iterators
let zipped: Vec<(&i32, &i32)> = a.iter().zip(b.iter()).collect();
// [(1,4), (2,5), (3,6)]

// enumerate: add index to each element
for (i, val) in a.iter().enumerate() {
    println!("index {i}: {val}");
}
// index 0: 1,  index 1: 2,  index 2: 3

// take: first N elements
let first_two: Vec<&i32> = a.iter().take(2).collect();
// [1, 2]

// skip: skip first N elements
let after_first: Vec<&i32> = a.iter().skip(1).collect();
// [2, 3]

// flatten: collapse nested iterators
let nested = vec![vec![1, 2], vec![3, 4], vec![5]];
let flat: Vec<&i32> = nested.iter().flatten().collect();
// [1, 2, 3, 4, 5]

// flat_map: map then flatten (very common)
let words: Vec<&str> = vec!["hello world", "foo bar"]
    .iter()
    .flat_map(|s| s.split_whitespace())
    .collect();
// ["hello", "world", "foo", "bar"]

// peekable: look ahead without consuming
let mut iter = [1, 2, 3].iter().peekable();
assert_eq!(iter.peek(), Some(&&1));   // peek without advancing
assert_eq!(iter.next(), Some(&1));     // now consume it

// windows and chunks (on slices, not iterators)
let data = [1, 2, 3, 4, 5];
for window in data.windows(3) {
    println!("{window:?}");            // [1,2,3], [2,3,4], [3,4,5]
}
for chunk in data.chunks(2) {
    println!("{chunk:?}");             // [1,2], [3,4], [5]
}

into_iter() vs iter() vs iter_mut()

The three iteration methods determine ownership semantics: whether you borrow, mutably borrow, or consume the collection.

Method Yields Collection After Use When
.iter() &T Unchanged (borrowed) You need to read elements without consuming
.iter_mut() &mut T Unchanged (mutably borrowed) You need to modify elements in place
.into_iter() T Consumed (moved) You want to take ownership of elements
let mut names = vec!["Alice".to_string(), "Bob".to_string(), "Charlie".to_string()];

// iter(): borrows each element as &String
for name in names.iter() {
    println!("Hello, {name}!");        // name is &String
}
// `names` is still usable here

// iter_mut(): mutably borrows each element as &mut String
for name in names.iter_mut() {
    name.push_str("!");                // modify in place
}
println!("{:?}", names);              // ["Alice!", "Bob!", "Charlie!"]

// into_iter(): takes ownership, consumes the collection
let owned: Vec<String> = names.into_iter()
    .map(|mut name| { name.push_str("?"); name })
    .collect();
// `names` is no longer valid here — it was consumed
println!("{:?}", owned);              // ["Alice!?", "Bob!?", "Charlie!?"]

// for loops use into_iter() implicitly
let items = vec![1, 2, 3];
for item in items {                    // same as items.into_iter()
    println!("{item}");
}
// `items` has been consumed

// for loops on references use iter() implicitly
let items = vec![1, 2, 3];
for item in &items {                   // same as items.iter()
    println!("{item}");
}
// `items` is still available

Custom Iterators

Implement the Iterator trait on your own types by defining a next() method. This unlocks the entire iterator combinator toolkit for your custom data structures.

// A counter that yields values from 1 up to a maximum
struct Counter {
    current: u32,
    max: u32,
}

impl Counter {
    fn new(max: u32) -> Self {
        Counter { current: 0, max }
    }
}

impl Iterator for Counter {
    type Item = u32;                   // associated type: what we yield

    fn next(&mut self) -> Option<Self::Item> {
        if self.current < self.max {
            self.current += 1;
            Some(self.current)
        } else {
            None                       // signals end of iteration
        }
    }
}

// Now Counter gets ALL iterator methods for free
let counter = Counter::new(5);
let sum: u32 = counter.sum();
println!("Sum 1..5 = {sum}");         // 15

let evens: Vec<u32> = Counter::new(10)
    .filter(|x| x % 2 == 0)
    .collect();
println!("{:?}", evens);              // [2, 4, 6, 8, 10]

// Zipping two custom iterators
let pairs: Vec<(u32, u32)> = Counter::new(3)
    .zip(Counter::new(3).map(|x| x * 10))
    .collect();
println!("{:?}", pairs);              // [(1, 10), (2, 20), (3, 30)]

// Fibonacci iterator
struct Fibonacci {
    a: u64,
    b: u64,
}

impl Fibonacci {
    fn new() -> Self {
        Fibonacci { a: 0, b: 1 }
    }
}

impl Iterator for Fibonacci {
    type Item = u64;

    fn next(&mut self) -> Option<Self::Item> {
        let result = self.a;
        self.a = self.b;
        self.b = result + self.b;
        Some(result)                   // infinite iterator — always returns Some
    }
}

let fibs: Vec<u64> = Fibonacci::new().take(10).collect();
println!("{:?}", fibs);
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
08

Error Handling

Rust distinguishes between unrecoverable errors (panic!) and recoverable errors (Result<T, E>). The type system forces you to handle errors explicitly — no unchecked exceptions.

panic! vs Recoverable Errors

panic! unwinds the stack and terminates the thread (or the program). Use it only for truly unrecoverable situations — bugs, violated invariants, or corrupted state. For everything else, use Result.

// panic! — unrecoverable error, aborts the thread
panic!("Something went terribly wrong");

// Common implicit panics
let v = vec![1, 2, 3];
// v[99];                              // panics: index out of bounds
// "hello".parse::<i32>().unwrap();   // panics: parse error

// Result<T, E> — recoverable error
use std::fs;
use std::io;

fn read_config() -> Result<String, io::Error> {
    fs::read_to_string("config.toml")  // returns Result<String, io::Error>
}

// Handle the Result explicitly
match read_config() {
    Ok(contents) => println!("Config: {contents}"),
    Err(e) => eprintln!("Failed to read config: {e}"),
}

// When to use panic! vs Result:
// panic!  → programmer bugs, impossible states, tests/prototyping
// Result  → expected failures (IO, network, user input, parsing)

The ? Operator & Result Chaining

The ? operator is Rust's primary tool for propagating errors. It unwraps an Ok value or returns the Err early from the enclosing function. It also works with Option (returning None on failure).

use std::fs;
use std::io;

// Without ? — verbose match chains
fn read_username_verbose() -> Result<String, io::Error> {
    let contents = match fs::read_to_string("username.txt") {
        Ok(s) => s,
        Err(e) => return Err(e),
    };
    Ok(contents.trim().to_string())
}

// With ? — clean and concise
fn read_username() -> Result<String, io::Error> {
    let contents = fs::read_to_string("username.txt")?;  // ? propagates Err
    Ok(contents.trim().to_string())
}

// Chaining multiple ? calls
fn get_user_email() -> Result<String, io::Error> {
    let username = fs::read_to_string("username.txt")?.trim().to_string();
    let email = fs::read_to_string(format!("emails/{username}.txt"))?;
    Ok(email.trim().to_string())
}

// ? with Option (in functions returning Option)
fn first_even(numbers: &[i32]) -> Option<i32> {
    let first = numbers.first()?;      // returns None if empty
    if first % 2 == 0 {
        Some(*first)
    } else {
        None
    }
}

// ? in main (requires main to return Result)
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = fs::read_to_string("app.toml")?;
    let port: u16 = config.trim().parse()?;
    println!("Running on port {port}");
    Ok(())
}

Multiple Error Types

When a function can produce different error types, you need a unified error type. The simplest approach is Box<dyn Error>. For libraries, define custom error enums.

use std::error::Error;
use std::fs;
use std::num::ParseIntError;
use std::io;

// Box<dyn Error> — the quick-and-easy approach
fn parse_config() -> Result<u16, Box<dyn Error>> {
    let text = fs::read_to_string("port.txt")?;   // io::Error
    let port = text.trim().parse::<u16>()?;        // ParseIntError
    Ok(port)
    // Both error types are automatically converted to Box<dyn Error>
}

// Custom error enum — full control (best for libraries)
#[derive(Debug)]
enum ConfigError {
    Io(io::Error),
    Parse(ParseIntError),
    Validation(String),
}

impl std::fmt::Display for ConfigError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ConfigError::Io(e) => write!(f, "IO error: {e}"),
            ConfigError::Parse(e) => write!(f, "Parse error: {e}"),
            ConfigError::Validation(msg) => write!(f, "Validation: {msg}"),
        }
    }
}

impl Error for ConfigError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            ConfigError::Io(e) => Some(e),
            ConfigError::Parse(e) => Some(e),
            ConfigError::Validation(_) => None,
        }
    }
}

// Implement From to enable ? conversion
impl From<io::Error> for ConfigError {
    fn from(e: io::Error) -> Self {
        ConfigError::Io(e)
    }
}

impl From<ParseIntError> for ConfigError {
    fn from(e: ParseIntError) -> Self {
        ConfigError::Parse(e)
    }
}

// Now ? works automatically
fn load_config() -> Result<u16, ConfigError> {
    let text = fs::read_to_string("port.txt")?;   // auto-converted via From
    let port = text.trim().parse::<u16>()?;        // auto-converted via From
    if port == 0 {
        return Err(ConfigError::Validation("Port cannot be 0".into()));
    }
    Ok(port)
}

thiserror Crate

The thiserror crate eliminates the boilerplate of manually implementing Display, Error, and From. It is the standard choice for library error types.

// Cargo.toml: thiserror = "2"
use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("IO error: {0}")]                         // generates Display impl
    Io(#[from] std::io::Error),                       // generates From impl

    #[error("Parse error: {0}")]
    Parse(#[from] std::num::ParseIntError),

    #[error("Validation failed: {message}")]
    Validation { message: String },

    #[error("Not found: {name}")]
    NotFound { name: String, id: u64 },

    #[error("Internal error")]
    Internal(#[source] Box<dyn std::error::Error + Send + Sync>),
}

// Usage: ? automatically converts thanks to #[from]
fn process_file(path: &str) -> Result<u64, AppError> {
    let text = std::fs::read_to_string(path)?;        // io::Error -> AppError::Io
    let value = text.trim().parse::<u64>()?;          // ParseIntError -> AppError::Parse
    if value == 0 {
        return Err(AppError::Validation {
            message: "Value must be non-zero".into(),
        });
    }
    Ok(value)
}

anyhow Crate

The anyhow crate provides anyhow::Result and anyhow::Error for ergonomic error handling in application code. It wraps any error type and provides rich context.

// Cargo.toml: anyhow = "1"
use anyhow::{Context, Result, bail, ensure};

// anyhow::Result<T> is an alias for Result<T, anyhow::Error>
fn load_config(path: &str) -> Result<Config> {
    let text = std::fs::read_to_string(path)
        .context("Failed to read config file")?;       // adds context message

    let config: Config = toml::from_str(&text)
        .with_context(|| format!("Failed to parse {path}"))?;  // lazy context

    Ok(config)
}

// bail! — return an error immediately
fn validate_port(port: u16) -> Result<()> {
    if port == 0 {
        bail!("Port cannot be zero");                  // returns Err(anyhow!(...))
    }
    Ok(())
}

// ensure! — assert-like macro that returns Err on failure
fn process(data: &[u8]) -> Result<()> {
    ensure!(!data.is_empty(), "Data must not be empty");
    ensure!(data.len() < 1024, "Data too large: {} bytes", data.len());
    // process data...
    Ok(())
}

// .context() adds human-readable messages for error chains
fn main() -> Result<()> {
    let config = load_config("app.toml")
        .context("Application startup failed")?;
    // Error output: "Application startup failed: Failed to read config file: ..."
    Ok(())
}

// Downcasting to the original error type
fn handle_error(err: &anyhow::Error) {
    if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
        eprintln!("IO error: {io_err}");
    } else {
        eprintln!("Other error: {err}");
    }
}

thiserror vs anyhow

The two crates serve complementary purposes and are commonly used together in the same project.

Aspect thiserror anyhow
Best for Libraries Applications
Error type Custom enum you define anyhow::Error (opaque wrapper)
Callers can match? Yes — match on variants Only via downcast
Context messages Manual (via Display) .context() / .with_context()
Boilerplate Minimal (derive macro) Near zero
When to choose When callers need to inspect error variants When you just need to propagate and report errors

Rule of Thumb

Library code: use thiserror — define explicit error types so your callers can match on them. Application code: use anyhow — you're the final consumer of errors, so ergonomics and context matter most. In a large project, libraries internal to the workspace use thiserror, and the top-level binary crate uses anyhow.

Common Error Handling Patterns

Practical patterns for dealing with errors in real Rust code.

// Pattern 1: Mapping errors with .map_err()
fn parse_port(s: &str) -> Result<u16, String> {
    s.parse::<u16>().map_err(|e| format!("Invalid port '{s}': {e}"))
}

// Pattern 2: Providing defaults with .unwrap_or() / .unwrap_or_else()
let port: u16 = std::env::var("PORT")
    .ok()                                   // Result -> Option
    .and_then(|s| s.parse().ok())           // parse, ignoring errors
    .unwrap_or(8080);                       // default value

// Pattern 3: Collecting Results — fail on first error
let strings = vec!["1", "2", "three", "4"];
let numbers: Result<Vec<i32>, _> = strings.iter()
    .map(|s| s.parse::<i32>())
    .collect();                             // Err on "three"
println!("{numbers:?}");                    // Err(ParseIntError)

// Pattern 4: Collecting Results — keep only successes
let numbers: Vec<i32> = strings.iter()
    .filter_map(|s| s.parse::<i32>().ok())
    .collect();
println!("{numbers:?}");                    // [1, 2, 4]

// Pattern 5: Converting between error types with From
impl From<serde_json::Error> for MyError {
    fn from(e: serde_json::Error) -> Self {
        MyError::Serialization(e.to_string())
    }
}

// Pattern 6: Using .ok() and .err() to convert between Result and Option
let result: Result<i32, &str> = Ok(42);
let opt: Option<i32> = result.ok();       // Some(42)

let result: Result<i32, &str> = Err("fail");
let opt: Option<&str> = result.err();     // Some("fail")

// Pattern 7: The try block (nightly feature, but common pattern via closures)
let result: Result<String, Box<dyn std::error::Error>> = (|| {
    let text = std::fs::read_to_string("data.txt")?;
    let value: i32 = text.trim().parse()?;
    Ok(format!("Got: {value}"))
})();
09

Traits & Generics

Traits define shared behavior (like interfaces). Generics let you write code that works across types. Together they power Rust's zero-cost polymorphism and enable powerful abstraction without runtime overhead.

Defining & Implementing Traits

A trait declares a set of methods a type must implement. Traits can provide default method bodies that implementors may override.

// Define a trait
trait Summary {
    // Required method (no body — implementors must define it)
    fn summarize(&self) -> String;

    // Default method (implementors can override or use as-is)
    fn preview(&self) -> String {
        format!("Read more: {}...", &self.summarize()[..20.min(self.summarize().len())])
    }
}

struct Article {
    title: String,
    author: String,
    content: String,
}

// Implement the trait for Article
impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{}, by {}", self.title, self.author)
    }
    // preview() uses the default implementation
}

struct Tweet {
    username: String,
    text: String,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("@{}: {}", self.username, self.text)
    }

    // Override the default
    fn preview(&self) -> String {
        format!("@{} tweeted", self.username)
    }
}

let article = Article {
    title: "Rust 2024".into(),
    author: "Ferris".into(),
    content: "Great year for Rust...".into(),
};
println!("{}", article.summarize());   // Rust 2024, by Ferris

// Traits with associated constants and multiple methods
trait Shape {
    const DIMENSIONS: u32;

    fn area(&self) -> f64;
    fn perimeter(&self) -> f64;
    fn description(&self) -> String {
        format!("A {}D shape with area {:.2}", Self::DIMENSIONS, self.area())
    }
}

Trait Bounds & impl Trait

Trait bounds constrain generic types to only those implementing specific traits. The impl Trait syntax provides a concise shorthand in function signatures.

// Trait bound syntax: T must implement Summary
fn notify<T: Summary>(item: &T) {
    println!("Breaking: {}", item.summarize());
}

// impl Trait shorthand (equivalent to the above for simple cases)
fn notify_short(item: &impl Summary) {
    println!("Breaking: {}", item.summarize());
}

// Multiple bounds with +
fn display_summary<T: Summary + std::fmt::Display>(item: &T) {
    println!("Summary: {}", item.summarize());
    println!("Display: {item}");
}

// Where clause (cleaner for many bounds)
fn process<T, U>(t: &T, u: &U) -> String
where
    T: Summary + Clone,
    U: std::fmt::Display + std::fmt::Debug,
{
    let copy = t.clone();
    format!("{} — {u}", copy.summarize())
}

// impl Trait in return position (return a type that implements Summary)
fn create_summary() -> impl Summary {
    Article {
        title: "New Article".into(),
        author: "Author".into(),
        content: "Content...".into(),
    }
}
// Caller sees only `impl Summary`, not the concrete `Article` type
// Note: you can only return ONE concrete type per function with impl Trait

dyn Trait (Trait Objects)

Trait objects (dyn Trait) enable dynamic dispatch — calling methods through a vtable at runtime. Use them when you need to store or return different types that implement the same trait.

// Box<dyn Trait> — heap-allocated trait object
fn make_summary(is_tweet: bool) -> Box<dyn Summary> {
    if is_tweet {
        Box::new(Tweet {
            username: "ferris".into(),
            text: "Hello, crabs!".into(),
        })
    } else {
        Box::new(Article {
            title: "News".into(),
            author: "Reporter".into(),
            content: "...".into(),
        })
    }
}

// Collections of different types via trait objects
let items: Vec<Box<dyn Summary>> = vec![
    Box::new(Article { title: "A".into(), author: "B".into(), content: "C".into() }),
    Box::new(Tweet { username: "user".into(), text: "hi".into() }),
];

for item in &items {
    println!("{}", item.summarize());  // dynamic dispatch
}

// &dyn Trait — borrowed trait object (no allocation)
fn print_summary(item: &dyn Summary) {
    println!("{}", item.summarize());
}

let tweet = Tweet { username: "ferris".into(), text: "Hello!".into() };
print_summary(&tweet);                // pass &Tweet as &dyn Summary
Aspect impl Trait (Static Dispatch) dyn Trait (Dynamic Dispatch)
Dispatch Compile-time (monomorphization) Runtime (vtable lookup)
Performance Faster (inlining possible) Small overhead per call
Binary size Larger (one copy per concrete type) Smaller (shared code)
Heterogeneous collections Not possible Yes — Vec<Box<dyn Trait>>
Return different types No (one concrete type per function) Yes — Box<dyn Trait>
Use when Type is known at compile time Type varies at runtime

Associated Types

Associated types are type placeholders within a trait that implementors fill in. They simplify signatures compared to generic parameters on the trait itself.

// Iterator uses an associated type rather than a generic parameter
// trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; }

struct Doubles {
    current: u32,
    max: u32,
}

impl Iterator for Doubles {
    type Item = u32;                   // specify the associated type

    fn next(&mut self) -> Option<Self::Item> {
        if self.current <= self.max {
            let val = self.current * 2;
            self.current += 1;
            Some(val)
        } else {
            None
        }
    }
}

// Custom trait with associated type
trait Converter {
    type Input;
    type Output;
    type Error;

    fn convert(&self, input: Self::Input) -> Result<Self::Output, Self::Error>;
}

struct CelsiusToFahrenheit;

impl Converter for CelsiusToFahrenheit {
    type Input = f64;
    type Output = f64;
    type Error = String;

    fn convert(&self, celsius: f64) -> Result<f64, String> {
        if celsius < -273.15 {
            Err("Below absolute zero".into())
        } else {
            Ok(celsius * 9.0 / 5.0 + 32.0)
        }
    }
}

// Why associated types vs generics on the trait?
// Associated type: one implementation per type (Iterator has ONE Item type)
// Generic param:   multiple implementations (From<T> can have many T values)

Generic Structs, Enums & Impl Blocks

Generics aren't just for functions — structs, enums, and their impl blocks can all be parameterized by types.

// Generic struct
struct Point<T> {
    x: T,
    y: T,
}

let int_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.5, y: 3.7 };

// Generic struct with multiple type parameters
struct Pair<A, B> {
    first: A,
    second: B,
}

let pair = Pair { first: "hello", second: 42 };

// Generic impl block
impl<T> Point<T> {
    fn new(x: T, y: T) -> Self {
        Point { x, y }
    }

    fn x(&self) -> &T {
        &self.x
    }
}

// Impl block with trait bounds (only for specific T)
impl<T: std::fmt::Display + PartialOrd> Point<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("x ({}) is >= y ({})", self.x, self.y);
        } else {
            println!("y ({}) is > x ({})", self.y, self.x);
        }
    }
}

// Impl for a specific concrete type
impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

// Generic enum (Option and Result are generic enums!)
enum Wrapper<T> {
    Some(T),
    Empty,
}

// Method on generic enum
impl<T: std::fmt::Debug> Wrapper<T> {
    fn inspect(&self) {
        match self {
            Wrapper::Some(val) => println!("Contains: {val:?}"),
            Wrapper::Empty => println!("Empty"),
        }
    }
}

Supertraits

A supertrait is a trait that requires another trait. If trait A: B, then any type implementing A must also implement B. This lets A's methods use B's methods.

use std::fmt;

// PrintableShape requires Display (supertrait)
trait PrintableShape: fmt::Display {
    fn area(&self) -> f64;

    fn print_info(&self) {
        // Can use Display methods because of the supertrait bound
        println!("{self} — area: {:.2}", self.area());
    }
}

struct Circle {
    radius: f64,
}

// Must implement BOTH Display and PrintableShape
impl fmt::Display for Circle {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Circle(r={})", self.radius)
    }
}

impl PrintableShape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

let c = Circle { radius: 5.0 };
c.print_info();                        // Circle(r=5) — area: 78.54

// Multiple supertraits
trait Serializable: fmt::Display + fmt::Debug + Clone {
    fn serialize(&self) -> String;
}

// Supertrait chains: A: B, B: C means A requires both B and C
trait Animal: fmt::Display {
    fn name(&self) -> &str;
}

trait Pet: Animal {                     // Pet requires Animal requires Display
    fn cuddle(&self) {
        println!("{} enjoys being cuddled!", self.name());
    }
}

Common Standard Library Traits

Rust's standard library defines many foundational traits. Knowing these is essential for writing idiomatic Rust.

Trait Purpose Derive? Notes
Display User-facing string formatting ({}) No Manual impl required; used by println!, format!
Debug Developer-facing formatting ({:?}) Yes #[derive(Debug)] on nearly every type
Clone Explicit deep copy via .clone() Yes Can be expensive; heap allocation
Copy Implicit bitwise copy (stack only) Yes Requires Clone; only for small stack types
PartialEq Equality comparison (==, !=) Yes Partial: NaN != NaN for floats
Eq Full equivalence (reflexive) Yes Marker trait; requires PartialEq
PartialOrd Ordering comparison (<, >, etc.) Yes Returns Option<Ordering>; partial for floats
Ord Total ordering Yes Requires Eq + PartialOrd; needed for BTreeMap keys
Hash Produce a hash value Yes Needed for HashMap/HashSet keys
Default Provide a default value Yes Type::default(); useful with struct update syntax
From / Into Type conversion No Implement From; Into is auto-derived. Enables ? for errors
Deref Custom dereference (*) No Enables deref coercion (e.g., &String&str)
Drop Custom destructor logic No Called automatically when value goes out of scope
Send Safe to transfer between threads Auto Auto-implemented; opt out with !Send
Sync Safe to share references between threads Auto Auto-implemented; T: Sync iff &T: Send
Iterator Produce a sequence of values No Requires type Item and fn next()

The Orphan Rule

Rust's coherence rules prevent conflicting trait implementations. The orphan rule states: you can only implement a trait for a type if you own the trait or you own the type (at least one must be local to your crate).

// ALLOWED: You own the type (MyStruct)
struct MyStruct(i32);

impl std::fmt::Display for MyStruct {   // Display is foreign, MyStruct is local — OK
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "MyStruct({})", self.0)
    }
}

// ALLOWED: You own the trait (MyTrait)
trait MyTrait {
    fn describe(&self) -> String;
}

impl MyTrait for Vec<i32> {            // MyTrait is local, Vec is foreign — OK
    fn describe(&self) -> String {
        format!("Vec with {} elements", self.len())
    }
}

// NOT ALLOWED: Both trait and type are foreign
// impl Display for Vec<i32> { ... }   // ERROR: neither Display nor Vec is local

// Workaround: the newtype pattern
struct Wrapper(Vec<String>);           // Wrapper is local

impl std::fmt::Display for Wrapper {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

// Use Deref to make the wrapper transparent
impl std::ops::Deref for Wrapper {
    type Target = Vec<String>;
    fn deref(&self) -> &Vec<String> {
        &self.0
    }
}

let w = Wrapper(vec!["a".into(), "b".into(), "c".into()]);
println!("{w}");                       // [a, b, c]
println!("Length: {}", w.len());       // Deref lets us call Vec methods

Orphan Rule Summary

You can implement ForeignTrait for LocalType or LocalTrait for ForeignType, but never ForeignTrait for ForeignType. When you need the latter, wrap the foreign type in a local newtype struct and optionally implement Deref so the wrapper is ergonomic to use.

10

Async & Concurrency

Rust offers multiple concurrency models: OS threads for parallel CPU work, async/await for efficient I/O, and data parallelism via Rayon. The type system enforces thread safety at compile time through the Send and Sync traits.

Threads: std::thread

Rust's standard library provides OS-level threads via std::thread::spawn. Each thread gets its own stack and runs independently. Use move closures to transfer ownership of captured variables into the thread.

use std::thread;

// Spawn a thread — returns a JoinHandle
let handle = thread::spawn(|| {
    for i in 1..=5 {
        println!("spawned thread: {i}");
        thread::sleep(std::time::Duration::from_millis(10));
    }
    42 // threads can return values
});

// Do work on the main thread concurrently
for i in 1..=3 {
    println!("main thread: {i}");
    thread::sleep(std::time::Duration::from_millis(10));
}

// Wait for the spawned thread to finish and get its return value
let result = handle.join().unwrap(); // blocks until thread completes
println!("Thread returned: {result}"); // 42

// move closures transfer ownership into the thread
let data = vec![1, 2, 3];
let handle = thread::spawn(move || {
    // `data` is now owned by this thread
    println!("Vector: {data:?}");
    data.len()
});
// println!("{data:?}");  // ERROR: data was moved into the thread
let len = handle.join().unwrap();
println!("Length: {len}");

// Spawning multiple threads
let mut handles = vec![];
for i in 0..5 {
    handles.push(thread::spawn(move || {
        println!("Thread {i} running");
        i * 2
    }));
}
let results: Vec<_> = handles.into_iter()
    .map(|h| h.join().unwrap())
    .collect();
println!("Results: {results:?}"); // [0, 2, 4, 6, 8]

Shared State: Arc, Mutex & RwLock

Arc<T> (atomic reference counting) allows shared ownership across threads. Combine it with Mutex<T> for exclusive mutable access or RwLock<T> for multiple readers / single writer.

use std::sync::{Arc, Mutex, RwLock};
use std::thread;

// Arc<Mutex<T>> — shared mutable state across threads
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
    let counter = Arc::clone(&counter); // clone the Arc, not the data
    handles.push(thread::spawn(move || {
        let mut num = counter.lock().unwrap(); // acquire lock
        *num += 1;
        // lock is released when `num` goes out of scope (RAII)
    }));
}

for handle in handles {
    handle.join().unwrap();
}
println!("Final count: {}", *counter.lock().unwrap()); // 10

// Arc<RwLock<T>> — multiple readers OR one writer
let config = Arc::new(RwLock::new(String::from("initial")));

// Multiple readers can read simultaneously
let config_r1 = Arc::clone(&config);
let config_r2 = Arc::clone(&config);
let r1 = thread::spawn(move || {
    let val = config_r1.read().unwrap(); // shared read lock
    println!("Reader 1: {val}");
});
let r2 = thread::spawn(move || {
    let val = config_r2.read().unwrap(); // another read lock — OK
    println!("Reader 2: {val}");
});

// Writer needs exclusive access
let config_w = Arc::clone(&config);
let w = thread::spawn(move || {
    let mut val = config_w.write().unwrap(); // exclusive write lock
    *val = String::from("updated");
});

r1.join().unwrap();
r2.join().unwrap();
w.join().unwrap();
println!("Config: {}", config.read().unwrap());

Message Passing: mpsc Channels

Channels provide a way to send data between threads without sharing memory. mpsc stands for multiple producer, single consumer. Clone the sender to create additional producers.

use std::sync::mpsc;
use std::thread;

// Create a channel — returns (Sender, Receiver)
let (tx, rx) = mpsc::channel();

thread::spawn(move || {
    let messages = vec!["hello", "from", "the", "thread"];
    for msg in messages {
        tx.send(msg).unwrap();
        thread::sleep(std::time::Duration::from_millis(50));
    }
    // tx is dropped here, closing the channel
});

// Receive messages (blocks until data arrives or channel closes)
for received in rx {
    println!("Got: {received}");
}

// Multiple producers — clone the sender
let (tx, rx) = mpsc::channel();
let tx2 = tx.clone(); // second producer

thread::spawn(move || {
    tx.send("from thread 1").unwrap();
});
thread::spawn(move || {
    tx2.send("from thread 2").unwrap();
});

// Receive from both producers
for msg in rx {
    println!("{msg}");
}

// Bounded channel with sync_channel (backpressure)
let (tx, rx) = mpsc::sync_channel(3); // buffer size of 3
thread::spawn(move || {
    for i in 0..10 {
        tx.send(i).unwrap(); // blocks when buffer is full
        println!("Sent: {i}");
    }
});
for val in rx {
    println!("Received: {val}");
    thread::sleep(std::time::Duration::from_millis(100));
}

Send and Sync Traits

These marker traits determine what can cross thread boundaries. The compiler enforces them automatically — you rarely implement them yourself, but understanding them explains many compiler errors.

Trait Meaning Examples
Send Ownership can be transferred to another thread Most types are Send. Rc<T> is not Send
Sync &T can be safely shared between threads Most types are Sync. Cell<T>, RefCell<T> are not Sync
Send + Sync Can be both moved and shared across threads Arc<Mutex<T>>, primitive types, String
!Send + !Sync Confined to a single thread Rc<T>, raw pointers, *const T
use std::rc::Rc;
use std::sync::Arc;

// Rc is NOT Send — this won't compile:
// let rc = Rc::new(42);
// std::thread::spawn(move || {
//     println!("{rc}"); // ERROR: Rc<i32> cannot be sent between threads
// });

// Arc IS Send + Sync — use it for cross-thread sharing
let arc = Arc::new(42);
let arc_clone = Arc::clone(&arc);
std::thread::spawn(move || {
    println!("{arc_clone}"); // OK: Arc is Send
}).join().unwrap();

// T: Send means we can move T into a thread
// T: Sync means &T is Send (safe to share references)
// Rule: T is Sync if and only if &T is Send

async/await & the Future Trait

async fn returns a Future that does nothing until .awaited. Futures are lazy — they represent a value that will be available later. An async runtime (like Tokio) drives futures to completion by polling them.

// async fn returns impl Future<Output = T>
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
    let response = reqwest::get(url).await?;  // .await suspends until ready
    let body = response.text().await?;
    Ok(body)
}

// Futures are lazy — nothing happens until you .await
let future = fetch_data("https://example.com"); // no request yet!
let result = future.await;                       // NOW the request runs

// The Future trait (simplified)
// trait Future {
//     type Output;
//     fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;
// }
// Poll is either Poll::Ready(value) or Poll::Pending

// async blocks — anonymous async expressions
let greeting = async {
    let name = get_name().await;
    format!("Hello, {name}!")
};
let message = greeting.await;

// You can use async with closures via async blocks
let tasks: Vec<_> = urls.iter().map(|url| {
    let url = url.to_string();
    async move {
        reqwest::get(&url).await?.text().await
    }
}).collect();

Tokio Runtime

Tokio is the most popular async runtime for Rust. It provides a multi-threaded scheduler, async I/O, timers, and utilities for building concurrent applications.

// Cargo.toml:
// [dependencies]
// tokio = { version = "1", features = ["full"] }

// #[tokio::main] sets up the runtime and runs your async main
#[tokio::main]
async fn main() {
    println!("Running in Tokio!");

    // tokio::spawn — spawn a concurrent task (like a lightweight thread)
    let handle = tokio::spawn(async {
        // This runs concurrently on the Tokio runtime
        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
        "task complete"
    });

    // JoinHandle — await to get the result
    let result = handle.await.unwrap(); // Returns Result for panic safety
    println!("{result}");

    // Spawn many tasks
    let mut handles = vec![];
    for i in 0..5 {
        handles.push(tokio::spawn(async move {
            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
            i * 2
        }));
    }
    for handle in handles {
        let val = handle.await.unwrap();
        println!("Got: {val}");
    }
}
// tokio::select! — wait for the first of multiple futures
use tokio::time::{sleep, Duration};

async fn task_a() -> &'static str {
    sleep(Duration::from_millis(100)).await;
    "A finished first"
}

async fn task_b() -> &'static str {
    sleep(Duration::from_millis(200)).await;
    "B finished first"
}

#[tokio::main]
async fn main() {
    // select! races multiple futures, runs the first to complete
    tokio::select! {
        result = task_a() => println!("{result}"),
        result = task_b() => println!("{result}"),
    }
    // Prints: "A finished first" (task_b is cancelled)

    // tokio::join! — run multiple futures concurrently, wait for ALL
    let (a, b) = tokio::join!(task_a(), task_b());
    println!("{a}, {b}"); // waits for both to complete
}

Async Traits (Rust 1.75+)

Since Rust 1.75, you can use async fn directly in trait definitions without external crates like async-trait. This makes async interfaces first-class in the language.

// Async fn in traits — stabilized in Rust 1.75
trait HttpClient {
    async fn get(&self, url: &str) -> Result<String, Box<dyn std::error::Error>>;
    async fn post(&self, url: &str, body: &str) -> Result<String, Box<dyn std::error::Error>>;
}

struct MyClient;

impl HttpClient for MyClient {
    async fn get(&self, url: &str) -> Result<String, Box<dyn std::error::Error>> {
        let resp = reqwest::get(url).await?;
        Ok(resp.text().await?)
    }

    async fn post(&self, url: &str, body: &str) -> Result<String, Box<dyn std::error::Error>> {
        let client = reqwest::Client::new();
        let resp = client.post(url).body(body.to_string()).send().await?;
        Ok(resp.text().await?)
    }
}

// Using the async trait
async fn fetch_page(client: &impl HttpClient) {
    let html = client.get("https://example.com").await.unwrap();
    println!("Got {} bytes", html.len());
}

// Note: for dyn dispatch with async traits, you may still need
// the #[trait_variant::make(SendHttpClient: Send)] attribute
// or return Pin<Box<dyn Future>> manually.

Rayon: Data Parallelism

Rayon makes data parallelism effortless. Replace .iter() with .par_iter() and Rayon automatically distributes work across CPU cores using a work-stealing thread pool.

// Cargo.toml:
// [dependencies]
// rayon = "1"

use rayon::prelude::*;

// par_iter() — parallel immutable iteration
let numbers: Vec<u64> = (0..1_000_000).collect();
let sum: u64 = numbers.par_iter().sum();
println!("Sum: {sum}");

// par_iter() with map and filter
let results: Vec<u64> = numbers.par_iter()
    .filter(|&&n| n % 2 == 0)
    .map(|&n| n * n)
    .collect();

// par_iter_mut() — parallel mutable iteration
let mut data: Vec<f64> = vec![1.0; 1_000_000];
data.par_iter_mut().for_each(|x| {
    *x = (*x * 2.5).sqrt();
});

// par_chunks() — process in parallel chunks
let pixels: Vec<u8> = vec![0; 1920 * 1080 * 4];
let brightened: Vec<u8> = pixels.par_chunks(4)
    .flat_map(|rgba| {
        [rgba[0].saturating_add(10),
         rgba[1].saturating_add(10),
         rgba[2].saturating_add(10),
         rgba[3]]
    })
    .collect();

// par_sort — parallel sorting
let mut data: Vec<i32> = (0..1_000_000).rev().collect();
data.par_sort(); // significantly faster than data.sort() for large collections

// Parallel string processing
let words = vec!["hello", "world", "foo", "bar", "baz"];
let upper: Vec<String> = words.par_iter()
    .map(|w| w.to_uppercase())
    .collect();

Threads vs Async vs Rayon

Choosing the right concurrency model depends on your workload. Here is a guide for when to reach for each approach.

Approach Best For Overhead Scales To Example Use Cases
std::thread Simple parallel work, background tasks ~8 KB stack per thread Dozens of threads Background file watcher, producer/consumer
Tokio (async) I/O-bound work: network, disk, timers ~200 bytes per task Millions of tasks Web servers, HTTP clients, database queries
Rayon CPU-bound data parallelism Thread pool (num CPUs) All CPU cores Image processing, number crunching, sorting

Choosing Your Concurrency Model

CPU-bound work (computation, data processing) → Rayon. I/O-bound work (networking, file I/O, databases) → Tokio. Simple parallel work (a few background threads) → std::thread. When in doubt: if you are processing a collection in parallel, use Rayon. If you are waiting on external resources, use Tokio. If you just need one or two background workers, use std::thread.

11

Cargo & Ecosystem

Cargo is Rust's build system, package manager, and project orchestrator. Combined with the rich crate ecosystem on crates.io, it provides everything from testing and documentation to workspaces and feature flags.

Cargo Deep Dive: Features, Workspaces & Profiles

Cargo supports workspaces for multi-crate projects, build profiles for optimization control, and a feature system for conditional compilation.

# Build profiles — dev (debug) and release are built-in
cargo build              # dev profile: fast compile, no optimizations
cargo build --release    # release profile: slow compile, full optimizations

# Custom profile in Cargo.toml
# [profile.dev]
# opt-level = 0          # no optimization (fast compile)
# debug = true           # include debug info
#
# [profile.release]
# opt-level = 3          # maximum optimization
# lto = true             # link-time optimization
# codegen-units = 1      # slower compile, better optimization
# strip = true           # strip debug symbols from binary

# Workspaces — multiple crates in one repository
# Root Cargo.toml:
# [workspace]
# members = [
#     "crates/core",
#     "crates/api",
#     "crates/cli",
# ]
# [workspace.dependencies]
# serde = { version = "1", features = ["derive"] }
# tokio = { version = "1", features = ["full"] }

# Member crates inherit workspace dependencies:
# [dependencies]
# serde = { workspace = true }

# Build specific workspace member
cargo build -p core
cargo test -p api
cargo run -p cli

Cargo.toml Features System

Features let you conditionally compile code and enable optional dependencies. Users of your crate choose which features to activate.

# Cargo.toml — defining features
# [features]
# default = ["json"]        # enabled unless the user opts out
# json = ["dep:serde_json"] # enables the serde_json dependency
# xml = ["dep:quick-xml"]   # enables the quick-xml dependency
# full = ["json", "xml"]    # meta-feature that enables others
#
# [dependencies]
# serde = "1"
# serde_json = { version = "1", optional = true }
# quick-xml = { version = "0.31", optional = true }

# Using features in code with cfg attributes
#[cfg(feature = "json")]
pub fn parse_json(input: &str) -> serde_json::Value {
    serde_json::from_str(input).unwrap()
}

#[cfg(feature = "xml")]
pub fn parse_xml(input: &str) -> String {
    // XML parsing logic
    todo!()
}

// Conditional compilation in expressions
fn serialize(data: &MyStruct) -> String {
    #[cfg(feature = "json")]
    { return serde_json::to_string(data).unwrap(); }

    #[cfg(not(feature = "json"))]
    { return format!("{data:?}"); }
}

# Adding a crate with specific features
# cargo add tokio --features full
# cargo add serde --features derive
# cargo add my-crate --no-default-features --features json

Testing

Rust has first-class testing built into the language and toolchain. Unit tests live alongside your code, integration tests go in a tests/ directory, and doc tests run code examples from documentation comments.

// Unit tests — live in the same file as the code they test
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

pub fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Division by zero".into())
    } else {
        Ok(a / b)
    }
}

#[cfg(test)]  // only compiled when running tests
mod tests {
    use super::*; // import everything from parent module

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }

    #[test]
    fn test_add_negative() {
        assert_eq!(add(-1, 1), 0);
    }

    #[test]
    fn test_divide() {
        let result = divide(10.0, 3.0).unwrap();
        assert!((result - 3.333).abs() < 0.01);
    }

    #[test]
    fn test_divide_by_zero() {
        assert!(divide(1.0, 0.0).is_err());
    }

    #[test]
    #[should_panic(expected = "index out of bounds")]
    fn test_panic() {
        let v = vec![1, 2, 3];
        let _ = v[99]; // panics
    }

    #[test]
    fn test_with_result() -> Result<(), String> {
        let result = divide(10.0, 2.0)?;
        assert_eq!(result, 5.0);
        Ok(()) // test passes if Ok is returned
    }
}
// Integration tests — live in tests/ directory (separate crate)
// tests/integration_test.rs
use my_crate::add;

#[test]
fn test_add_integration() {
    assert_eq!(add(100, 200), 300);
}

// Doc tests — code in documentation comments is tested automatically
/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// let result = my_crate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
///
/// ```
/// // Negative numbers work too
/// assert_eq!(my_crate::add(-1, 1), 0);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

// Running tests:
// cargo test                     — run all tests
// cargo test test_add            — run tests matching "test_add"
// cargo test --lib               — only unit tests
// cargo test --test integration  — only integration tests
// cargo test --doc               — only doc tests
// cargo test -- --nocapture      — show println! output

Modules & Visibility

Rust's module system controls code organization and visibility. Items are private by default. Use pub and its variants to expose what you need.

// Declaring modules
mod math {
    // Everything inside is private by default
    fn internal_helper() -> i32 { 42 }

    pub fn add(a: i32, b: i32) -> i32 { a + b }

    // pub(crate) — visible within the entire crate but not to external users
    pub(crate) fn crate_only() -> &'static str { "internal API" }

    // pub(super) — visible to the parent module only
    pub(super) fn parent_only() -> &'static str { "parent can see me" }

    // Nested modules
    pub mod advanced {
        pub fn multiply(a: i32, b: i32) -> i32 { a * b }

        // Can access parent's private items with super::
        pub fn add_and_multiply(a: i32, b: i32) -> i32 {
            super::add(a, b) * 2
        }
    }
}

// Using modules
use math::add;
use math::advanced::multiply;
let sum = add(1, 2);
let product = multiply(3, 4);

// Module file structure (modern style — preferred since Rust 2018)
// src/
// ├── main.rs           (or lib.rs)
// ├── math.rs           ← mod math;
// └── math/
//     └── advanced.rs   ← pub mod advanced; (inside math.rs)

// Legacy style (still works, less preferred)
// src/
// ├── main.rs
// └── math/
//     ├── mod.rs        ← mod math;
//     └── advanced.rs   ← pub mod advanced; (inside mod.rs)

// Re-exporting with pub use
mod internal {
    pub struct Config { pub name: String }
}
pub use internal::Config; // users can import Config directly

Smart Pointers

Smart pointers own heap data and provide extra capabilities beyond regular references. Each serves a different use case for ownership, sharing, and interior mutability.

Type Purpose Thread Safe? When to Use
Box<T> Heap allocation, single owner Yes (if T: Send) Recursive types, large data, trait objects
Rc<T> Reference-counted shared ownership No Multiple owners, single thread only
Arc<T> Atomic reference-counted sharing Yes Multiple owners across threads
RefCell<T> Interior mutability (runtime borrow check) No Mutate through shared references (single thread)
Cell<T> Interior mutability for Copy types No Simple counters, flags in shared contexts
Cow<'a, T> Clone-on-write: borrowed or owned Yes (if T: Send) Avoid cloning when mutation is rare
use std::cell::RefCell;
use std::rc::Rc;
use std::borrow::Cow;

// Box — heap allocation, recursive types
enum List {
    Cons(i32, Box<List>),
    Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));

// Box for trait objects
let shapes: Vec<Box<dyn Shape>> = vec![
    Box::new(Circle { radius: 5.0 }),
    Box::new(Rect { w: 3.0, h: 4.0 }),
];

// Rc — shared ownership (single-threaded)
let data = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&data); // increment ref count, not deep copy
let clone2 = Rc::clone(&data);
println!("Ref count: {}", Rc::strong_count(&data)); // 3

// Rc<RefCell<T>> — shared + mutable (single-threaded)
let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
shared.borrow_mut().push(4);          // runtime borrow check
println!("{:?}", shared.borrow());     // [1, 2, 3, 4]

// Cow — clone only when needed
fn process(input: &str) -> Cow<'_, str> {
    if input.contains("bad") {
        Cow::Owned(input.replace("bad", "good")) // allocates only if needed
    } else {
        Cow::Borrowed(input) // zero-cost: just borrows the input
    }
}

Collections Overview

Rust's standard library provides several high-performance collection types. Here are the most commonly used ones.

Collection Description Access Use When
Vec<T> Growable array (contiguous memory) O(1) index, O(1) amortized push Default sequential collection
HashMap<K, V> Hash table (key-value pairs) O(1) average lookup/insert Key-value lookups, counting, caching
HashSet<T> Hash set (unique values) O(1) average contains/insert Membership testing, deduplication
BTreeMap<K, V> Sorted map (B-tree) O(log n) lookup/insert Need sorted keys, range queries
VecDeque<T> Double-ended queue (ring buffer) O(1) push/pop front and back Queue or deque semantics
BinaryHeap<T> Max-heap priority queue O(log n) push/pop, O(1) peek Priority queues, top-N problems
use std::collections::{HashMap, HashSet, BTreeMap, VecDeque, BinaryHeap};

// Vec — the workhorse collection
let mut v = vec![1, 2, 3];
v.push(4);
v.extend([5, 6, 7]);
let third = v[2];           // panics if out of bounds
let safe = v.get(99);       // returns Option<&T>

// HashMap — key-value store
let mut scores: HashMap<&str, i32> = HashMap::new();
scores.insert("Alice", 100);
scores.insert("Bob", 85);
scores.entry("Charlie").or_insert(0); // insert if absent
*scores.entry("Alice").or_insert(0) += 10; // modify in place

// HashSet — unique values
let mut seen = HashSet::new();
seen.insert("apple");
seen.insert("banana");
seen.insert("apple"); // no effect — already present
println!("Contains apple: {}", seen.contains("apple"));

// BTreeMap — sorted by key
let mut ordered = BTreeMap::new();
ordered.insert(3, "three");
ordered.insert(1, "one");
ordered.insert(2, "two");
for (k, v) in &ordered {
    println!("{k}: {v}"); // prints in key order: 1, 2, 3
}

// VecDeque — efficient push/pop at both ends
let mut deque = VecDeque::new();
deque.push_back(1);
deque.push_front(0);
deque.push_back(2);
println!("{:?}", deque); // [0, 1, 2]

// BinaryHeap — max-heap
let mut heap = BinaryHeap::from(vec![3, 1, 4, 1, 5]);
println!("Max: {:?}", heap.peek()); // Some(5)
while let Some(val) = heap.pop() {
    print!("{val} "); // 5 4 3 1 1
}

Essential Crates

The Rust ecosystem has a rich set of high-quality crates for common tasks. These are the most widely used and recommended libraries.

Crate Purpose Key Feature
serde Serialization / deserialization framework #[derive(Serialize, Deserialize)] for JSON, TOML, YAML, etc.
tokio Async runtime (networking, I/O, timers) #[tokio::main], spawn, channels, timers
clap Command-line argument parsing #[derive(Parser)] for declarative CLI definitions
reqwest HTTP client (async and blocking) Ergonomic API for GET, POST, JSON, file uploads
sqlx Async SQL toolkit (compile-time checked queries) sqlx::query! verifies SQL at compile time against your DB
axum Web framework (built on Tokio + Tower) Ergonomic routing, extractors, middleware via Tower
tracing Structured logging and diagnostics Spans, events, async-aware structured logs
rayon Data parallelism .par_iter() for effortless parallel processing
regex Regular expressions Fast, safe regex engine with compile-time validation via Regex::new
chrono Date and time handling Timezone-aware datetime, parsing, formatting

Macros

Macros generate code at compile time. Declarative macros (macro_rules!) use pattern matching on syntax. Derive macros automatically implement traits for your types.

// macro_rules! — declarative macros
macro_rules! say_hello {
    () => {
        println!("Hello!");
    };
    ($name:expr) => {
        println!("Hello, {}!", $name);
    };
}
say_hello!();          // "Hello!"
say_hello!("Ferris");  // "Hello, Ferris!"

// Repetition with $(...)*
macro_rules! vec_of_strings {
    ($($s:expr),* $(,)?) => {
        vec![$($s.to_string()),*]
    };
}
let names = vec_of_strings!["Alice", "Bob", "Charlie"];

// A more useful example: a hashmap literal macro
macro_rules! hashmap {
    ($($key:expr => $val:expr),* $(,)?) => {{
        let mut map = std::collections::HashMap::new();
        $(map.insert($key, $val);)*
        map
    }};
}
let scores = hashmap! {
    "Alice" => 100,
    "Bob" => 85,
    "Charlie" => 92,
};

// Common fragment specifiers:
// $x:expr   — expression (1 + 2, foo(), "hello")
// $x:ident  — identifier (variable name, function name)
// $x:ty     — type (i32, Vec<String>)
// $x:pat    — pattern (Some(x), (a, b))
// $x:stmt   — statement (let x = 5;)
// $x:tt     — single token tree (most flexible)
// Derive macros — auto-implement traits
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct User {
    name: String,
    age: u32,
}

// Common derives and what they give you:
// Debug         — {:?} formatting for println!/dbg!
// Clone         — .clone() deep copy
// Copy          — implicit bitwise copy (only for small stack types)
// PartialEq     — == and != comparison
// Eq            — marker that PartialEq is total (reflexive)
// Hash          — can be used as HashMap/HashSet key
// Default       — Type::default() returns a zero/empty value
// PartialOrd    — <, >, <=, >= comparison
// Ord           — total ordering (required for BTreeMap keys)

// Serde derives (from the serde crate)
use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
struct Config {
    name: String,
    port: u16,
    #[serde(default)]
    debug: bool,
    #[serde(rename = "maxRetries")]
    max_retries: u32,
}

let json = r#"{"name":"app","port":8080,"maxRetries":3}"#;
let config: Config = serde_json::from_str(json).unwrap();
let back_to_json = serde_json::to_string_pretty(&config).unwrap();
12

Tips & Tricks

Practical tips, useful patterns, and productivity boosters for writing idiomatic and performant Rust code.

Turbofish Syntax (::<>)

The turbofish ::<> lets you explicitly specify type parameters on functions and methods when the compiler cannot infer them. The name comes from its resemblance to a fish: ::<>.

// When the compiler can't infer the type, use turbofish
let numbers = "1 2 3 4 5";
let parsed: Vec<i32> = numbers.split(' ')
    .map(|s| s.parse::<i32>().unwrap())  // turbofish on parse
    .collect();

// Equivalent: specify the collection type instead
let parsed = numbers.split(' ')
    .map(|s| s.parse::<i32>().unwrap())
    .collect::<Vec<_>>();  // turbofish on collect

// You need turbofish when:
// 1. The return type alone can't determine the generic parameter
let x = "42".parse::<i32>().unwrap();
let y = "3.14".parse::<f64>().unwrap();

// 2. Calling a generic function with ambiguous type
fn create<T: Default>() -> T { T::default() }
let s = create::<String>();
let n = create::<i32>();

// 3. On collect when the target type isn't otherwise constrained
let set = vec![1, 2, 2, 3].into_iter().collect::<std::collections::HashSet<_>>();

dbg!() Macro for Quick Debugging

dbg!() prints the file, line number, expression, and its value to stderr, then returns the value. It is perfect for quick debugging without disrupting data flow.

let a = 2;
let b = dbg!(a * 3);      // prints: [src/main.rs:2] a * 3 = 6
// b is now 6 — dbg! returns the value

// Works in chains without breaking them
let result = dbg!(vec![1, 2, 3])
    .iter()
    .map(|x| dbg!(x * 2))
    .sum::<i32>();
dbg!(result);

// Multiple values
dbg!(a, b, result);
// [src/main.rs:10] a = 2
// [src/main.rs:10] b = 6
// [src/main.rs:10] result = 12

// Tip: dbg! requires Debug trait. It goes to stderr (not stdout)
// so it won't interfere with program output.
// Remove all dbg!() calls before committing!

Useful Attributes

Attributes annotate items with metadata that affects compilation, linting, and behavior. Here are the most practical ones.

// #[must_use] — warn if the return value is ignored
#[must_use = "this Result may contain an error that should be handled"]
fn connect(url: &str) -> Result<Connection, Error> { /* ... */ }

connect("localhost"); // WARNING: unused Result that must be used

// #[non_exhaustive] — prevent external code from exhaustively matching
#[non_exhaustive]
pub enum Error {
    NotFound,
    PermissionDenied,
    // Future variants can be added without breaking external code
}
// External match must have a _ wildcard arm

// #[allow(...)] / #[deny(...)] / #[warn(...)] — control lints
#[allow(dead_code)]        // suppress unused code warnings
#[allow(unused_variables)] // suppress unused variable warnings
fn experimental() { let x = 42; }

#[deny(unsafe_code)]       // make any unsafe code a compile error
mod safe_module { /* ... */ }

// #[cfg(...)] — conditional compilation
#[cfg(target_os = "linux")]
fn linux_only() { println!("Linux!"); }

#[cfg(test)]
mod tests { /* ... */ }

#[cfg(debug_assertions)]
fn debug_only_check() { /* runs only in debug builds */ }

#[cfg(feature = "advanced")]
pub mod advanced { /* compiled only when feature is enabled */ }

// #[inline] — hint the compiler to inline a function
#[inline]
fn small_helper(x: i32) -> i32 { x + 1 }

#[inline(always)] // force inlining (use sparingly)
fn critical_path(x: i32) -> i32 { x * 2 }

Cargo Aliases & RUSTFLAGS

Configure Cargo with custom aliases and compiler flags for a smoother development workflow.

# .cargo/config.toml — project-level Cargo configuration
# [alias]
# t = "test"
# r = "run"
# c = "check"
# cl = "clippy -- -W clippy::pedantic"
# b = "build --release"
# w = "watch -x check -x test"    # requires cargo-watch

# Now you can run:
# cargo t          → cargo test
# cargo cl         → cargo clippy with pedantic lints

# RUSTFLAGS — pass flags to the Rust compiler
# RUSTFLAGS="-D warnings" cargo build    # treat all warnings as errors
# RUSTFLAGS="-C target-cpu=native" cargo build --release  # optimize for your CPU

# Useful clippy flags
cargo clippy -- -W clippy::pedantic           # enable pedantic lints
cargo clippy -- -W clippy::nursery            # experimental lints
cargo clippy -- -A clippy::must_use_candidate # allow specific lint
cargo clippy --fix                             # auto-fix what it can

# Per-project lint configuration in Cargo.toml:
# [lints.clippy]
# pedantic = "warn"
# cast_possible_truncation = "allow"
# module_name_repetitions = "allow"

Performance Tips

Squeezing maximum performance from Rust requires the right build settings and awareness of common optimization opportunities.

# Cargo.toml — release profile for maximum performance
# [profile.release]
# opt-level = 3          # max optimization
# lto = true             # link-time optimization (slower build, faster binary)
# codegen-units = 1      # single codegen unit (slower build, better optimization)
# panic = "abort"        # smaller binary, no unwinding overhead
# strip = true           # strip debug symbols

# Build with native CPU instructions
# RUSTFLAGS="-C target-cpu=native" cargo build --release

# Profile-guided optimization (PGO)
# 1. Build instrumented binary:
#    RUSTFLAGS="-Cprofile-generate=/tmp/pgo" cargo build --release
# 2. Run with typical workload to generate profile data
# 3. Build optimized binary:
#    RUSTFLAGS="-Cprofile-use=/tmp/pgo/merged.profdata" cargo build --release
// Code-level performance tips

// Preallocate collections when size is known
let mut v = Vec::with_capacity(1000); // avoid repeated reallocations
for i in 0..1000 { v.push(i); }

// Use iterators instead of index loops (often optimized better)
let sum: i32 = v.iter().sum();        // idiomatic, vectorizable
// vs: for i in 0..v.len() { sum += v[i]; }  // bounds checks each access

// Avoid unnecessary clones
fn process(data: &str) { /* ... */ }  // borrow instead of clone
// fn process(data: String) { ... }   // forces caller to clone or give up ownership

// Use &str instead of &String in function params
fn greet(name: &str) {}               // accepts both &String and &str

// Prefer stack allocation (arrays) over heap (Vec) for small fixed sizes
let buffer: [u8; 256] = [0; 256];    // stack allocated
// let buffer = vec![0u8; 256];      // heap allocated

// Use Box<[T]> instead of Vec<T> for fixed-size heap data
let boxed: Box<[i32]> = vec![1, 2, 3].into_boxed_slice(); // no capacity overhead

Common Design Patterns

Idiomatic Rust patterns that leverage the type system for safer, more expressive APIs.

// Builder Pattern — ergonomic construction of complex objects
struct Server {
    host: String,
    port: u16,
    max_connections: usize,
    timeout_secs: u64,
}

struct ServerBuilder {
    host: String,
    port: u16,
    max_connections: usize,
    timeout_secs: u64,
}

impl ServerBuilder {
    fn new(host: impl Into<String>) -> Self {
        ServerBuilder {
            host: host.into(),
            port: 8080,          // sensible defaults
            max_connections: 100,
            timeout_secs: 30,
        }
    }
    fn port(mut self, port: u16) -> Self { self.port = port; self }
    fn max_connections(mut self, n: usize) -> Self { self.max_connections = n; self }
    fn timeout(mut self, secs: u64) -> Self { self.timeout_secs = secs; self }
    fn build(self) -> Server {
        Server {
            host: self.host,
            port: self.port,
            max_connections: self.max_connections,
            timeout_secs: self.timeout_secs,
        }
    }
}

let server = ServerBuilder::new("localhost")
    .port(3000)
    .max_connections(500)
    .timeout(60)
    .build();
// Newtype Pattern — type safety through wrapping
struct UserId(u64);
struct OrderId(u64);

// These are different types — can't accidentally mix them!
fn get_user(id: UserId) { /* ... */ }
fn get_order(id: OrderId) { /* ... */ }
// get_user(OrderId(42));  // ERROR: expected UserId, found OrderId

// Typestate Pattern — encode state in the type system
struct Locked;
struct Unlocked;

struct Door<State> {
    _state: std::marker::PhantomData<State>,
}

impl Door<Locked> {
    fn unlock(self) -> Door<Unlocked> {
        println!("Door unlocked");
        Door { _state: std::marker::PhantomData }
    }
}

impl Door<Unlocked> {
    fn open(&self) { println!("Door opened"); }
    fn lock(self) -> Door<Locked> {
        println!("Door locked");
        Door { _state: std::marker::PhantomData }
    }
}

let door: Door<Locked> = Door { _state: std::marker::PhantomData };
// door.open();          // ERROR: no method `open` for Door<Locked>
let door = door.unlock(); // transition to Unlocked state
door.open();              // OK: Door<Unlocked> has open()

unsafe Basics

unsafe does not disable the borrow checker — it unlocks five specific capabilities that the compiler cannot verify. Use it only when necessary, and keep unsafe blocks as small as possible.

// What unsafe permits (and ONLY these five things):
// 1. Dereference raw pointers
// 2. Call unsafe functions or methods
// 3. Access mutable static variables
// 4. Implement unsafe traits
// 5. Access fields of unions

// Raw pointers — *const T and *mut T
let mut x = 42;
let r1 = &x as *const i32;       // creating raw pointer is safe
let r2 = &mut x as *mut i32;
unsafe {
    println!("r1 = {}", *r1);    // dereferencing requires unsafe
    *r2 = 99;                     // writing through raw pointer
    println!("r2 = {}", *r2);
}

// Calling unsafe functions (e.g., FFI)
extern "C" {
    fn abs(input: i32) -> i32;
}
let result = unsafe { abs(-42) }; // calling C function

// Safe wrapper around unsafe code (common pattern)
fn safe_abs(x: i32) -> i32 {
    unsafe { abs(x) }
}

// When you need unsafe:
// - FFI (calling C libraries)
// - Performance-critical code where bounds checks are too expensive
// - Implementing low-level data structures (e.g., linked lists)
// - Interfacing with hardware or OS APIs

// Golden rule: encapsulate unsafe in a safe API
// and document the safety invariants

Rust Edition Migration

Rust editions (2015, 2018, 2021, 2024) introduce new syntax and behavior. Editions are opt-in and backward-compatible — crates on different editions interoperate seamlessly.

# Check your current edition in Cargo.toml:
# [package]
# edition = "2021"

# Migrate to a new edition
cargo fix --edition          # auto-fix code for the next edition
# Then manually update Cargo.toml:
# edition = "2024"

# Key edition changes:
# 2018: module path changes, async/await, dyn Trait required
# 2021: disjoint capture in closures, IntoIterator for arrays
# 2024: gen blocks, unsafe_op_in_unsafe_fn lint, RPITIT refinement

# Edition does NOT affect the language version — only syntax/semantics
# A Rust 1.85 compiler can build crates using edition 2015, 2018, 2021, or 2024
# Different crates in a workspace can use different editions

Useful Cargo Subcommands

Extend Cargo with community-built tools. Install with cargo install.

Command Install Purpose
cargo expand cargo install cargo-expand Show expanded macro output (see what macros generate)
cargo tree Built-in Visualize dependency tree, find duplicate deps
cargo audit cargo install cargo-audit Check dependencies for known security vulnerabilities
cargo deny cargo install cargo-deny Lint dependencies for licenses, bans, advisories
cargo watch cargo install cargo-watch Auto-rebuild on file changes (cargo watch -x check)
cargo flamegraph cargo install flamegraph Generate flamegraph profiles for performance analysis
cargo nextest cargo install cargo-nextest Faster test runner with better output and parallelism
# Useful cargo tree commands
cargo tree                          # full dependency tree
cargo tree -d                       # show only duplicate dependencies
cargo tree -i regex                 # show what depends on regex
cargo tree --depth 1                # top-level dependencies only

# cargo watch examples
cargo watch -x check                # recheck on save
cargo watch -x "test -- --nocapture"  # retest with output
cargo watch -x run                  # rerun on save

# cargo audit
cargo audit                         # check for vulnerabilities
cargo audit fix                     # auto-fix where possible

Common Clippy Lints to Enable

Clippy provides hundreds of lints beyond what rustc checks. Here are high-value lint groups and individual lints to consider enabling project-wide.

// In main.rs or lib.rs — enable lints globally
#![warn(clippy::pedantic)]          // stricter style lints
#![warn(clippy::nursery)]           // newer, possibly unstable lints
#![warn(clippy::cargo)]             // Cargo.toml best practices

// Or in Cargo.toml (Rust 1.74+):
// [lints.clippy]
// pedantic = "warn"
// nursery = "warn"
// unwrap_used = "warn"             // prefer ? or expect() over unwrap()
// expect_used = "warn"             // be explicit about panic points
// dbg_macro = "warn"               // catch leftover dbg!() calls
// todo = "warn"                    // catch leftover todo!() macros
// print_stdout = "warn"            // use tracing/log instead of println!
// cast_possible_truncation = "allow"  # too noisy for many projects
// module_name_repetitions = "allow"   # allow module::ModuleThing naming

// Commonly allowed pedantic lints (too strict for most projects):
#![allow(clippy::must_use_candidate)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]

Community Resources

The Rust community maintains excellent learning resources, from beginner-friendly tutorials to advanced references.

Resource URL Description
The Rust Programming Language doc.rust-lang.org/book The official book — best starting point for learning Rust
Rust by Example doc.rust-lang.org/rust-by-example Learn Rust through annotated, runnable code examples
Rustlings github.com/rust-lang/rustlings Small exercises to practice Rust syntax and concepts
This Week in Rust this-week-in-rust.org Weekly newsletter covering Rust news, crates, and blog posts
Rust Reference doc.rust-lang.org/reference Precise language specification and grammar details
std Library Docs doc.rust-lang.org/std Complete API documentation for the standard library
crates.io crates.io The Rust package registry — search and discover crates
Rust Playground play.rust-lang.org Run Rust code in the browser — great for quick experiments