Rust syntax reference

The compressed essence of Rust Book ch. 1–13 · built for lookup while typing, not for reading

Keep this open in a tab while you write code. Every line here is something you already met in ch1–13 — the point is to stop it blocking you mid-sentence.

Cargo commands

cargo new my_proj        # create project (binary)
cargo new my_lib --lib   # create library
cargo run                # build + run
cargo run -- 95 83       # pass args to your program (after --)
cargo build              # build only (debug)
cargo build --release    # optimized build
cargo check              # typecheck fast, no binary
cargo test               # run #[test] functions
cargo add rand           # add a dependency

Book: 1.3 Hello, Cargo!

Variables

let x = 5;               // immutable
let mut y = 5;           // mutable
y = 6;                   // ok, y is mut
const MAX: u32 = 100;    // const: always typed, UPPER_CASE
let x = 5;
let x = x + 1;           // shadowing: new variable, same name

Book: 3.1

Types

i8 i16 i32 i64 i128 isize    // signed ints   (i32 = default)
u8 u16 u32 u64 u128 usize    // unsigned ints (usize = index/len type)
f32 f64                      // floats        (f64 = default)
bool                         // true / false
char                         // 'a' — 4 bytes, Unicode scalar

let t: (i32, f64) = (500, 6.4);   // tuple
let (a, b) = t;                   // destructure
let first = t.0;                  // index

let arr: [u16; 10] = [0; 10];     // array: fixed length, 10 zeros
let arr = [1, 2, 3];              // inferred [i32; 3]

let s = "hi";                     // &str  — borrowed, fixed
let s = String::from("hi");       // String — owned, growable
let n: u32 = "42".parse().unwrap();      // str -> number
let n = "42".parse::<u32>().unwrap();    // same, turbofish form
let s = 42.to_string();                  // number -> String
let len = arr.len() as u32;              // numeric cast

Book: 3.2

Functions

fn add(a: i32, b: i32) -> i32 {
    a + b            // no semicolon = this is the return value
}

fn shout(msg: &str) -> String {
    msg.to_uppercase()
}

fn log(msg: &str) {  // no -> means returns ()
    println!("{msg}");
}

Statement vs expression: a line ending in ; is a statement (produces no value). Drop the ; and it is an expression whose value is returned. return x; works too, but is only idiomatic for early returns.

Book: 3.3

Control flow

if n < 5 { ... } else if n < 10 { ... } else { ... }
let label = if n > 0 { "pos" } else { "neg" };   // arms must be same type

loop { break; }                     // infinite until break
let got = loop { break 7; };        // loop can return a value
while n < 10 { n += 1; }
for i in 0..5 { }                   // 0,1,2,3,4
for i in 0..=5 { }                  // 0..5 inclusive
for item in &vec { }                // borrow each item
for (i, c) in word.char_indices() { }

Book: 3.5

Ownership — the three rules

  1. Each value has exactly one owner.
  2. There can be only one owner at a time.
  3. When the owner goes out of scope, the value is dropped.
let s1 = String::from("hi");
let s2 = s1;             // MOVE — s1 is now invalid
// println!("{s1}");     // compile error: borrow of moved value

let s2 = s1.clone();     // deep copy — both usable, costs allocation

let n1 = 5;
let n2 = n1;             // COPY — i32 is Copy, n1 still valid

Copy types (stack-only, fixed size): all integers, f32/f64, bool, char, and tuples containing only Copy types. Everything heap-owning (String, Vec) moves instead.

Book: 4.1

References & borrowing

fn length(s: &String) -> usize { s.len() }        // borrow, read-only
fn push_bang(s: &mut String) { s.push('!'); }     // borrow, mutable

let mut s = String::from("hi");
length(&s);        // pass an immutable reference
push_bang(&mut s); // pass a mutable reference

The borrowing rule: at any one time you may have either one mutable reference or any number of immutable references — never both. Enforced at compile time; this is what rules out data races without a GC.

Book: 4.2

Slices

let s = String::from("hello world");
let hello = &s[0..5];    // &str — pointer + length, owns nothing
let world = &s[6..11];
let whole = &s[..];

let arr = [1, 2, 3, 4, 5];
let part: &[i32] = &arr[1..3];   // [2, 3]

fn total(nums: &[u32]) -> u32 { ... }   // takes Vec or array — prefer &[T]

Book: 4.3

Structs

struct Rectangle { width: u32, height: u32 }        // named fields
struct Point(i32, i32);                             // tuple struct
struct Marker;                                      // unit struct

#[derive(Debug)]                    // enables {:?} printing
struct User { name: String, active: bool }

let r = Rectangle { width: 30, height: 50 };
println!("{r:?}");                  // needs #[derive(Debug)]

impl Rectangle {
    fn new(width: u32, height: u32) -> Self {   // associated fn (no self)
        Self { width, height }                  // field init shorthand
    }
    fn area(&self) -> u32 {                     // method: borrows
        self.width * self.height
    }
    fn scale(&mut self, by: u32) {              // method: borrows mutably
        self.width *= by;
    }
    fn consume(self) -> u32 { self.width }      // method: takes ownership
}

Rectangle::new(3, 4).area();        // :: for associated fn, . for method

Book: 5.1–5.3

Enums & pattern matching

enum Shape {
    Circle(f64),                    // variant with data
    Rect { w: f64, h: f64 },        // variant with named fields
    Empty,                          // variant with no data
}

enum Option<T> { Some(T), None }             // in std — "maybe a value"
enum Result<T, E> { Ok(T), Err(E) }          // in std — "value or error"

match shape {
    Shape::Circle(r) => 3.14 * r * r,
    Shape::Rect { w, h } => w * h,
    Shape::Empty => 0.0,
}                                   // must be exhaustive

match score {
    90..=100 => "A",                // range pattern
    n if n > 50 => "pass",          // match guard
    _ => "F",                       // catch-all
}

if let Some(x) = maybe { ... }               // one pattern, ignore rest
if let Some(x) = maybe { ... } else { ... }
while let Some(top) = stack.pop() { ... }    // loop while pattern matches

Book: 6.1–6.3

Modules & paths

// src/lib.rs or src/main.rs — the crate root
mod front_of_house;              // loads src/front_of_house.rs (or .../mod.rs)

mod hosting {                     // inline module
    pub fn add_to_waitlist() {}   // pub = visible outside this module
    fn seat() {}                  // private (default)
}

crate::front_of_house::hosting::add_to_waitlist();  // absolute path
front_of_house::hosting::add_to_waitlist();         // relative path
super::hosting::add_to_waitlist();                  // parent module

use crate::front_of_house::hosting;      // bring into scope
use std::collections::HashMap;
use std::io::{self, Read};               // multiple items
use std::fmt::Result as FmtResult;       // rename
pub use crate::hosting;                  // re-export

Everything is private by default. A child module can see its parent's items; a parent needs pub to see into a child. pub on a struct does not make its fields public — each field needs its own pub.

Book: 7.1–7.5

Packages vs crates — the two-crate package

my_proj/
├── Cargo.toml       # ONE package
├── src/
│   ├── lib.rs       # crate 1: the LIBRARY, named after the package
│   ├── thing.rs     # a module inside crate 1
│   └── main.rs      # crate 2: the BINARY — a separate crate
└── tests/
    └── spec.rs      # crate 3: integration tests — separate again

Cargo finds all three by filename. No [lib] or [[bin]] in Cargo.toml is needed.

// src/thing.rs — INSIDE the library crate
use crate::other::Thing;         // crate:: = root of the crate I am in

// src/main.rs — a DIFFERENT crate
use my_proj::thing::Thing;       // must use the package name

// tests/spec.rs — also a different crate
use my_proj::thing::Thing;       // same as main.rs

use crate::.. in main.rs gives error[E0432]: unresolved import. When a path will not resolve, ask: which crate am I in right now?

Three wiring states, in the order you hit them:

In lib.rsResult
nothingE0433: cannot find `thing` — a file is not a module until declared
mod thing;E0603: module `thing` is private
pub mod thing;works

Integration tests in tests/ can only reach pub items via the library crate — they cannot see inside main.rs at all. That is the reason to put logic in lib.rs and keep main.rs thin.

Book: 7.1 Packages and Crates · 11.3 Test Organization

Common derives

#[derive(Debug, Clone, Copy, PartialEq)]
enum Size { Small, Large }
DeriveGivesNeeded for
Debug{:?}test failure output, logs
PartialEq==assert_eq! on your type
Clone.clone()explicit second copy
Copyassignment copies, not movessmall types, no heap data
PartialOrd, Ord<, .sort()ordering; enum variant order = the ordering

A type containing a String or Vec cannot be Copy. Forget a derive and the compiler names the exact trait and type.

Book: Appendix C — Derivable Traits

Collections

// Vec — owned, growable list
let mut v: Vec<u32> = Vec::new();
let v = vec![1, 2, 3];
v.push(4);
let third = &v[2];               // panics if out of range
let third = v.get(2);            // returns Option<&u32> — safe
v.get(9).unwrap_or(&0);          // default if missing
for n in &v { }                  // iterate borrowed
for n in &mut v { *n += 1; }     // iterate mutably
v.len(); v.is_empty(); v.pop(); v.contains(&3);

// String — owned, growable, UTF-8
let mut s = String::new();
s.push_str("hi");  s.push('!');
let s = format!("{a}-{b}");
let joined = words.join(" ");
for w in text.split_whitespace() { }
for (i, c) in text.char_indices() { }
text.to_uppercase(); text.trim(); text.len();   // len = BYTES, not chars

// HashMap — key/value
use std::collections::HashMap;
let mut m = HashMap::new();
m.insert("a", 10);
m.get("a");                      // Option<&i32>
let count = m.entry(key).or_insert(0);   // insert-if-absent, returns &mut
*count += 1;                             // the counting idiom
for (k, v) in &m { }

Book: 8.1–8.3

Iterators

// ONE trait, one method — everything else is a default method built on next()
pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

// THREE ways in — the &self / &mut self / self choice, one element at a time
v.iter()        // Item = &T        reading; collection survives
v.iter_mut()    // Item = &mut T    editing in place; collection survives
v.into_iter()   // Item = T         taking the elements; collection consumed

// ADAPTERS — lazy, return an iterator, compose freely
.map(|x| ..) .filter(|x| ..) .enumerate() .skip(n) .take(n) .rev() .zip(other)

// CONSUMERS — do the work, return something that is not an iterator
.collect() .find(|x| ..) .position(|x| ..) .any(|x| ..) .all(|x| ..)
.count() .sum() .max() .min() .fold(init, |acc, x| ..) .for_each(|x| ..)

// find gives the ITEM, position gives the INDEX — both Option, both take ok_or
list.iter().find(|t| t.id == id).ok_or(NotFound(id))?       // -> &T
list.iter().position(|t| t.id == id).ok_or(NotFound(id))?   // -> usize

list.retain(|t| t.keep);   // Vec method, not Iterator: delete in place, one pass

// COLLECT builds any FromIterator type — you must say which
let v: Vec<String> = it.collect();
let s: String      = it.collect();          // String: FromIterator<String>
let m: HashMap<K, V> = pairs.collect();     // from an iterator of (K, V)
let r: Result<Vec<T>, E> = it.collect();    // SHORT-CIRCUITS on the first Err
let o: Option<Vec<T>>    = it.collect();    // .. and on the first None

text.lines()     // trailing "\n" is a TERMINATOR — no empty last item, strips \r
text.split('\n')      // trailing "\n" is a SEPARATOR — yields a final ""

Errors you will see:

warning: unused `Map` that must be used
  = note: iterators are lazy and do nothing unless consumed   // no consumer

error[E0283]: type annotations needed
  | let x = it.collect();                    ------- type must be known at this point
  = note: multiple `impl`s satisfying `_: FromIterator<String>` found

error[E0502]: cannot borrow `self.v` as immutable because it is also borrowed as mutable
  // an iter_mut() result in a variable holds the borrow until its LAST use

error[E0507]: cannot move out of `x.field` which is behind a shared reference
  // a closure over &T cannot give away an owned field — Copy, clone, or &str

Cost: an adapter chain is a tower of small structs compiled to one pass — no temporary vectors, no runtime penalty against the equivalent for loop.

When a loop still wins: folding many items into one accumulator you mutate. Iterators replace loops that search, transform, or collect.

Book: 13.2 Iterators · std: Iterator · FromIterator

HashMap keys & counting

use std::collections::HashMap;

// A key must be Eq + Hash: hash to pick the bucket, compare inside it.
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]   // Copy for fieldless enums
enum Priority { Low, Medium, High }

let mut m: HashMap<Priority, usize> = HashMap::new();
*m.entry(key).or_insert(0) += 1;   // THE counting idiom: one lookup, &mut V back
m.entry(key).or_default();               // same, using Default::default()
m.get(&key).copied().unwrap_or(0);       // Option<&usize> -> usize, absent = 0
for (k, v) in &m { }         // ARBITRARY order — impose your own before printing
HashMap::from([(Priority::High, 2)]);    // literal, for tests

Eq is a marker: no methods, one extra promise over PartialEq — every value equals itself. f64 breaks it (NAN != NAN), so f64 cannot be a key.

Want sorted keys instead of arbitrary order? BTreeMap, same API, K: Ord.

Book: 8.3 Hash maps · std: Eq · Hash

Error handling

panic!("boom");                  // unrecoverable — stops the program

// Recoverable: return Result and let the caller decide
fn read_name() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;    // ? = return Err early
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)                                     // must wrap success in Ok
}

match File::open("x.txt") {
    Ok(f) => f,
    Err(e) => match e.kind() {
        ErrorKind::NotFound => File::create("x.txt").unwrap(),
        _ => panic!("{e:?}"),
    },
};

r.unwrap();               // value, or panic
r.expect("message");      // value, or panic with your message
r.unwrap_or(0);           // value, or fallback
r.unwrap_or_else(|e| 0);  // value, or compute fallback
r.is_ok(); r.is_err();

fn main() -> Result<(), Box<dyn Error>> { ... }   // main can return Result

Rule of thumb: panic! / unwrap in tests, prototypes, and truly impossible states. Result everywhere else — especially in library code and anything a backend service depends on.

Book: 9.1–9.3

The Result pattern

enum Result<T, E> { Ok(T), Err(E) }    // an enum: errors are VALUES
enum Option<T>    { Some(T), None }    // absence, with no reason attached

Pick the shortest rung that fits:

match r { Ok(v) => ..., Err(e) => ... }   // full control, both arms required
if let Err(e) = r { ... }                 // only care about failure
r.unwrap_or(default)                      // fallback value
r.unwrap_or_else(|e| compute())           // fallback, computed
r.ok()                                    // Result -> Option (drops the reason)
r.map(|v| v + 1)                          // change Ok, leave Err untouched
r.is_ok() / r.is_err()                    // just ask
r.unwrap() / r.expect("why")              // value, or PANIC
r?                                        // hand the error to my caller

What ? really does:

let v = thing()?;
// expands to roughly:
let v = match thing() {
    Ok(value) => value,
    Err(e) => return Err(From::from(e)),   // early exit + TYPE CONVERSION
};

The From::from step is why ? can mix error types in one function:

fn load_port(path: &str) -> Result<u16, Box<dyn Error>> {
    let text = fs::read_to_string(path)?;    // io::Error      -\
    let port = text.trim().parse::<u16>()?;  // ParseIntError -> Box<dyn Error>
    Ok(port)
}

? requires the enclosing function to return Result or Option:

error[E0277]: the `?` operator can only be used in a function that returns `Result`
  | cannot use the `?` operator in a function that returns `()`
help: consider adding return type

Fix the signature, not the ?. Even main can return Result — end it with Ok(()).

Dropping a Result is a warning, not silence:

warning: unused `Result` that must be used
  = note: this `Result` may be an `Err` variant, which should be handled
help: use `let _ = ...` to ignore the resulting value

panic vs propagate: unwrap/expect in tests, prototypes, and states that truly cannot happen. Result for anything touching files, args, network, or users. In a service: propagate with ? through the inner layers, and decide what the user sees at one boundary (the request handler).

Custom error types — the recipe

#[derive(Debug)]                          // Error requires Debug
enum SensorError {
    Empty,                                // no data
    NotANumber(ParseFloatError),          // wrap the cause
    OutOfRange(f64),                      // keep the bad value
}

impl fmt::Display for SensorError {       // the human sentence
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            SensorError::Empty => write!(f, "no reading given"),
            SensorError::NotANumber(_) => write!(f, "reading is not a number"),
            SensorError::OutOfRange(v) => write!(f, "{v} is outside -90..60"),
        }
    }
}

impl Error for SensorError {}       // std::error::Error — marker, no body needed

// …or, when a variant WRAPS another error, hand the cause to source() instead:
impl Error for SensorError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            SensorError::NotANumber(e) => Some(e),   // the wrapped cause
            _ => None,                               // nothing underneath
        }
    }
}

impl From<ParseFloatError> for SensorError {   // makes bare `?` convert for you
    fn from(e: ParseFloatError) -> SensorError { SensorError::NotANumber(e) }
}

Three obligations, in order: #[derive(Debug)], then Display, then the empty impl Error. Skip Display and the marker impl fails:

error[E0277]: `SensorError` doesn't implement `std::fmt::Display`
13 | impl Error for SensorError {}
   |                ^^^^^^^^^^^ unsatisfied trait bound

From is a trait with one method — fn from(value: T) -> Self, i.e. "how to build me out of a T". String::from("hi") is the same trait. Three equivalent spellings once the impl exists:

Err(SensorError::from(e))     // call it yourself
Err(e.into())              // same trait, from the value's side (Into comes free)
text.parse::<f64>()?          // `?` calls From::from(e) for you

// what `?` expands to:
match thing() { Ok(v) => v, Err(e) => return Err(From::from(e)) }

Missing From impl, seen through ?:

error[E0277]: `?` couldn't convert the error to `SensorError`
28 |     Ok(text.parse::<f64>()?)
   |             --------------^ the trait `From<ParseFloatError>` is not implemented for `SensorError`

// with an annotated `let`, the same missing impl is reported as:
error[E0271]: type mismatch resolving `<f64 as FromStr>::Err == SensorError`
29 |     let value: f64 = text.parse()?;
   |                           ^^^^^ expected `SensorError`, found `ParseFloatError`

ok_or("text")? in a function returning Result<_, String> already relies on this: std ships impl From<&str> for String.

Reporting it in a CLI — main -> Result prints with {:?} (Debug), so handle it yourself when a human reads the output:

if let Err(e) = run(&args, &mut store) {
    eprintln!("error: {e}");    // stderr, Display
    process::exit(1);           // status a script can test
}

Wrapped cause: source() or Display, never both. std's own rule — "the underlying error should be either returned by the outer error's Error::source(), or rendered by the outer error's Display implementation, but not both." So write your one sentence in Display, drop the {e}, and expose the cause through source() for logs and --verbose.

let e = parse_reading("nope").unwrap_err();
e.to_string()                      // "reading is not a number"  <- yours
e.source().map(|s| s.to_string())  // Some("invalid float literal")  <- std's

Reviewer's checklist for an error type (API Guidelines C-GOOD-ERR): implements Error; is Send + Sync (needed to cross threads, so needed by every web framework); never (); Display message lowercase, no trailing punctuation, concise. Assert the bounds in a test with an empty generic fn:

fn assert_usable_as_error<E: Error + Send + Sync + 'static>() {}
assert_usable_as_error::<TaskError>();   // compile-time check, no body needed

Matching without naming every variant — matches! returns a bool:

assert!(matches!(err, TaskError::BadId(_)));      // true if the shape matches
if matches!(status, Status::Todo | Status::InProgress) { .. }

Adding a variant is a compiler-enforced review:

error[E0004]: non-exhaustive patterns: `&TaskError::StoreFull` not covered
19 |         match self {
   |               ^^^^ pattern `&TaskError::StoreFull` not covered
note: `TaskError` defined here … 14 |     StoreFull,  --------- not covered

That failure is the reason to prefer an enum over a String — and the reason not to write _ => .. when matching your own error type at the reporting edge.

Two more errors from the same migration:

error[E0432]: unresolved import `crate::error`   // `pub mod error;` missing
error[E0369]: binary operation `==` cannot be applied to type `TaskError`
note: `TaskError` does not implement `PartialEq`  // tests use assert_eq!

Book: 9.2 · std: Error · From

Files, io::Error & FromStr

use std::fs;
use std::io::{self, ErrorKind};
use std::path::{Path, PathBuf};

fs::read_to_string(path)?   // io::Result<String>  = Result<String, io::Error>
fs::write(path, text)?   // io::Result<()>       — creates or truncates, one call
text.lines()                // iterator of &str, no trailing newlines

// a path from the environment, with a default
let path = PathBuf::from(
    env::var("TASKS_FILE").unwrap_or_else(|_| "tasks.txt".to_string()));

One io failure is not another — e.kind() returns an ErrorKind. A missing file on first run is expected; everything else is not. Use a match guard:

let text = match fs::read_to_string(path) {
    Ok(text) => text,
    Err(e) if e.kind() == ErrorKind::NotFound => return Ok(Store::new()),
    Err(e) => return Err(TaskError::Io(e)),               // permissions, disk, …
};

FromStr is the trait behind .parse(). Implement it and .parse::<YourType>() starts working — type Err is an associated type: a slot the implementor fills once, not a parameter the caller passes.

pub trait FromStr: Sized {
    type Err;
    fn from_str(s: &str) -> Result<Self, Self::Err>;
}

impl FromStr for Task {
    type Err = TaskError;
    fn from_str(line: &str) -> Result<Task, TaskError> {
        let bad = || TaskError::BadLine(line.to_string());  // built on failure
        let mut parts = line.splitn(4, '|');  // splitn: last piece keeps its '|'
        let id: u32 = parts.next().ok_or_else(bad)?.parse().map_err(|_| bad())?;
        ..
    }
}
error[E0046]: not all trait items implemented, missing: `Err`   // no `type Err`
error[E0277]: the trait bound `Task: FromStr` is not satisfied  // no impl

? vs map_err: only one impl From<T> for YourError may exist per T (a second is error[E0119]: conflicting implementations). So ? handles the canonical meaning, and map_err names the variant for every other meaning of the same error type.

let id: u32 = text.parse()?;                                // -> BadId, via From
let id: u32 = text.parse().map_err(|_| bad())?;        // -> BadLine, chosen here

Not everything derives. io::Error is not PartialEq, so a variant holding one breaks #[derive(PartialEq)] (E0369). Write it by hand; mem::discriminant covers the payload-free variants:

impl PartialEq for TaskError {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {                         // match on a TUPLE of both
            (Io(a), Io(b)) => a.kind() == b.kind(),
            (NotFound(a), NotFound(b)) => a == b,
            _ => std::mem::discriminant(self) == std::mem::discriminant(other),
        }
    }
}

Book: 12.2, 12.5 · std: fs · ErrorKind · FromStr

Traits & generics

trait Reading {
    fn celsius(&self) -> f64;             // REQUIRED — semicolon
    fn label(&self) -> String {        // DEFAULT — has a body, may be overridden
        format!("{:.1}C", self.celsius())
    }
}

impl Reading for Kettle {                  // impl TRAIT for TYPE
    fn celsius(&self) -> f64 { (self.fahrenheit - 32.0) * 5.0 / 9.0 }
}

impl fmt::Display for Thermometer {        // the trait behind `{}`
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} [{:.1}C]", self.room, self.celsius)   // INTO f, no `;`
    }
}

Bounds — four spellings, one meaning ("any T that implements Reading"):

fn show<T: Reading>(r: &T)                       // angle brackets
fn show(r: &impl Reading)                        // shorthand
fn show<T>(r: &T) where T: Reading               // where clause, for long lists
fn show(r: &dyn Reading)                  // trait OBJECT: type chosen at runtime
fn show<T: Reading + Clone>(r: &T)               // two promises at once

Box<dyn Error> is the same dyn idea: "some heap value that implements Error". Use it when the failures are unrelated and an enum is not worth writing.

A generic function of your own — one definition, one specialised copy compiled per set of types actually used (monomorphisation), so the generality is free at runtime:

pub fn tally<T, K, F>(items: &[T], key: F) -> HashMap<K, usize>
where
    K: Eq + Hash,      // required by the HashMap being returned, not by the body
    F: Fn(&T) -> K,        // each closure has its OWN anonymous type
{
    let mut counts = HashMap::new();
    for item in items { *counts.entry(key(item)).or_insert(0) += 1; }
    counts
}

tally(&tasks, |t| t.priority)            // T = Task,  K = Priority
tally(&["a", "bb"], |w| w.len())         // T = &str,  K = usize

The clause is a contract both ways: callers must satisfy it, and the body may use only what it promises. Fn = borrows its captures · FnMut = mutates them · FnOnce = consumes them. Take Fn unless you need more.

Errors you will see:

error[E0046]: not all trait items implemented, missing: `celsius`
   | ^^^^^^^^^^^^^^^^^^^^^^^ missing `celsius` in implementation

error[E0277]: `Kettle` doesn't implement `std::fmt::Display`
   = note: in format strings you may be able to use `{:?}` instead

error[E0599]: no method named `celsius` found for reference `&&T` in the current scope
   = help: items from traits can only be used if the trait is implemented and in scope
note: `Reading` defines an item `celsius`, perhaps you need to implement it

That last one is the missing-bound error: a bare <T> promises nothing, so no method exists on it. Add the bound.

Free consequences of one impl: Display grants ToString (so .to_string() works); From<A> for B grants A.into(): B and makes ? convert.

Orphan rule: impl Trait for Type is allowed only if the trait or the type is yours. Display for MyTask ✓ · MyTrait for Vec<T> ✓ · Display for Vec<String> ✗.

Trait in scope: to call a trait method you must have the trait imported (use std::io::Write; before .write_all()). println!("{}") is exempt — the macro names Display by full path.

Book: 10.2 Traits · 10.1 Generics · std: Display

Printing

println!("plain text");
println!("{}", value);           // Display
println!("{value}");             // inline variable (preferred)
println!("{value:?}");           // Debug — needs #[derive(Debug)]
println!("{value:#?}");          // Debug, pretty-printed
eprintln!("to stderr");

Debug vs Display

Two independent traits. A type can have one, both, or neither.

{}    {value}     needs Display — text for END USERS
{:?}  {value:?}   needs Debug   — text for PROGRAMMERS
{:#?} {value:#?}  needs Debug   — same, multi-line
let s = String::from("hi\tthere");
println!("{s}");     // hi	there        <- raw, for a user
println!("{s:?}");   // "hi\tthere"     <- quoted + escaped, exact
#[derive(Debug)]                 // compiler writes Debug for you
struct Config { name: String }

impl fmt::Display for Point {    // Display is NEVER derivable — write by hand
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

Why no derive(Display): how a value should look to a human is your program's decision, not something the compiler can guess. Debug has one obvious form (type name + fields), so it can be generated.

Why Vec has Debug but no Display: there is no single correct way to show a list to a user (commas? bullets? brackets?), so std refuses to pick. You choose:

println!("{v:?}");                 // ["a", "b"]     — diagnostics
println!("{}", v.join(", "));      // a, b           — you pick the format

Errors you will see:

error[E0277]: `Vec<String>` doesn't implement `std::fmt::Display`
  = note: in format strings you may be able to use `{:?}` instead

error[E0277]: `Point` doesn't implement `Debug`
  = note: add `#[derive(Debug)]` to `Point` or manually `impl Debug for Point`

Rule of thumb: use {:?} while developing and in logs. Write a Display impl only when a real person reads the output. Custom error types want both — Display for the caller's message, Debug for the log.

Book: 5.2 (derive Debug) · 10.2 Traits · std::fmt docs

Tests

#[cfg(test)]              // compiled by `cargo test`, not by `cargo build`
mod tests {
    use super::*;         // the child module needs its parent's items

    #[test]                             // no arguments, no return value
    fn a_task_starts_as_todo() {
        assert_eq!(Task::new(1, "x", Low).status, Status::Todo);
    }
}

A test fails by panicking. Each test runs on its own thread; the harness marks it failed when that thread dies. So every assertion macro is a wrapper that panics, and unwrap/expect in a test body is a legitimate assertion.

assert!(cond)              // prints the SOURCE TEXT of cond
assert!(cond, "id {} was wrong", id)     // extra args go to format!
assert_eq!(a, b)           // prints both values as `left` / `right`
assert_ne!(a, b)           // same, passes when they differ

assert_eq! needs PartialEq (to compare) and Debug (to print) on the values — that is what the #[derive(Debug, PartialEq)] on your own types is for. Prefer it over assert!(a == b), which throws the values away. An assertion inside a loop needs a message naming the case.

Two ways to test a failure — one per way your code fails:

#[test]
#[should_panic(expected = "at most 30")]   // substring of the panic message
fn refuses_a_high_target() { Thermostat::new(99); }

#[test]
fn a_reload_sees_what_was_saved() -> Result<(), TaskError> {
    store.save(&path)?;                    // `?` for SETUP that must work
    assert!(Store::load(&bad).is_err());   // is_err for the tested failure
    Ok(())
}

Always give should_panic its expected, or any panic passes the test. #[should_panic] and a Result return cannot be combined; assert with is_err() or unwrap_err() instead. A Result test's error type only needs Debug.

Two homes, two levels of access:

Unit testIntegration test
Lives inbottom of the source filetests/<name>.rs
Compiled aspart of your crateits own separate crate
Can reachprivate items toothe public API only
Needs#[cfg(test)] + use super::*use mycrate::…
Run alonecargo test --libcargo test --test name

Nothing in src/main.rs is testable — a binary crate exposes nothing to use. Keep main thin, put the logic in the library, and take the output destination as a parameter so a test can read it:

pub fn run(args: &[String], store: &mut Store, out: &mut impl Write)
    -> Result<(), TaskError>
{
    writeln!(out, "added task {}", id)?;   // never println! in library code
}

run(&args, &mut store, &mut io::stdout().lock())?;   // in main
let mut out: Vec<u8> = Vec::new();                   // in a test
run(&args, &mut store, &mut out)?;
assert_eq!(String::from_utf8(out)?, "added task 1\n");

Helpers shared by two integration files go in tests/common/mod.rs, never tests/common.rs — a plain file there is compiled as its own test crate and shows up as a stray running 0 tests section.

tests/
├── common/mod.rs     mod common;  then  common::three_tasks()
├── cli.rs            #![allow(dead_code)] in mod.rs: each crate uses some
└── mine.rs

Tests run in parallel and share nothing safely — no shared file, no current directory, no environment variable. Give every file-touching test its own path (process id + an AtomicU32 counter, inside env::temp_dir()).

cargo test                     # everything
cargo test warmer              # every test whose FULL name contains it
cargo test --lib               # unit tests only   (--test cli = one file)
cargo test -- --show-output    # also print stdout of tests that PASSED
cargo test -- --ignored        # only the #[ignore] ones
cargo test -- --test-threads=1 # no parallelism — diagnose, do not fix

Flags before -- go to cargo, flags after it go to the test binary. The module path is part of a test's name (task::tests::…), so filtering on a module name runs that module.

running 4 tests
test tests::refuses_a_high_target - should panic ... ok
test tests::slow_one ... ignored, only when the range changes

test result: ok. 3 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
// measured = nightly benchmarks · filtered out = excluded by your filter

Errors you will see:

error[E0433]: cannot find type `Thermostat` in this scope   // no use super::*
error[E0603]: function `capped` is private        // private fn, from tests/
error[E0616]: field `target` of struct `Thermostat` is private
  help: a method `target` also exists, call it with parentheses

Judge a suite by the bugs it kills, not by the number of tests. Change one operator in the code by hand, run the suite, and put it back: if nothing went red, that behaviour is untested.

Book: 11.1 Writing tests · 11.2 Running them · 11.3 Organization