Language & library reference
Concept clusters →Every named result in this course, grouped by chapter. Each one links back to where it's derived.
Set up Rust
Rule
Compile, then run
also known as: ahead-of-time compilation, cargo run = build + run
Formal. Rust is compiled ahead of time: rustc translates the whole
program into a native executable before any of it runs, and refuses to
produce one if the program has an error. cargo run rebuilds whatever
changed and then runs the new executable; cargo check stops after the
checking step and makes no executable at all.
In plain English. Nothing you type runs until the compiler has turned all of it into a program your computer can start. If the compiler finds a mistake anywhere, you get no program, only the error.
Example. Edit src/main.rs, then run the old executable in
target/debug/ directly: you still see the old behavior. cargo run
notices the edit, recompiles, and runs the new version.
Why this rule exists. Doing all the checking before the program starts is what lets Rust catch whole classes of bugs, and it is why a compiled Rust program needs no runtime installed to run.
How Rust code builds and runs
Rule
Editions are chosen per crate
also known as: edition = "2024", Rust editions
Formal. Each crate declares one edition in its Cargo.toml
(edition = "2024"). The edition decides which syntax and lint defaults
the compiler applies to that crate's own code. Crates of different
editions link together freely, and every new stable compiler still
builds every older edition.
In plain English. An edition is a setting a project chooses, not a separate version of Rust you install. Your project can use the 2024 rules while a library it depends on still uses 2021, and they work together.
Example. Let chains (if let Some(x) = a && x > 0) compile in a
crate with edition = "2024" and are rejected in one with
edition = "2021", on the same compiler.
Why this rule exists. It lets the language fix old mistakes without breaking the millions of lines of code already written.
Values, types, and variables
Rule
Variables are immutable by default
also known as: let vs let mut, E0384
Formal. A binding introduced with let cannot be assigned to again;
assigning to it is error E0384. Only a binding declared let mut may be
assigned after it is initialized.
In plain English. Once you give a name a value, Rust assumes it stays
that value. If you plan to change it, you say so up front with mut.
Example. let count = 0; count += 1; is error E0384.
let mut count = 0; count += 1; compiles.
Why this rule exists. Most values never need to change, and a reader
(or the compiler) can trust a name that can't change. The mut marks
exactly the places worth watching.
Rule
Integer overflow panics in debug builds and wraps in release builds
also known as: arithmetic overflow, checked_add, wrapping_add
Formal. When an integer operation's true result doesn't fit its type,
a debug build (overflow checks on, the default for cargo run) panics
with "attempt to add with overflow"; a release build (overflow checks
off) wraps around modulo 2ⁿ. The explicit methods checked_*,
wrapping_*, saturating_* and overflowing_* behave the same in both.
In plain English. If a number gets too big for its box, your debug
build stops loudly so you notice, and the optimized build quietly wraps
around. When you care, say what you want with a method such as
checked_add.
Example. For let x: u8 = 255;, x + 1 panics in a debug build and
is 0 in a release build; x.checked_add(1) is None in both.
Why this rule exists. Silent wraparound is a classic source of security bugs, but checking every addition costs speed in the final program. Rust checks where it's cheap to find bugs and lets you choose explicitly where it matters.
Rule
as never fails: it truncates, wraps, or saturates
also known as: numeric cast, as vs TryFrom
Formal. A numeric as cast always produces a value. Integer to
smaller integer keeps the low bits; float to integer rounds toward zero
and saturates at the target's limits (NaN becomes 0). From is provided
only where no information can be lost, and TryFrom/try_into return a
Result for conversions that can fail.
In plain English. as is the "just do it" conversion: it never
complains, even when it has to throw information away. Use from when
nothing can be lost, and try_from when it might be, so you find out.
Example. 300_i32 as u8 is 44; u8::try_from(300_i32) is an
Err; u32::from(200_u8) is 200.
Why this rule exists. Low-level code sometimes needs the raw bit behavior, but ordinary code should be told when a conversion loses data.
Expressions and control flow
Rule
Blocks are expressions
also known as: tail expression, statements vs expressions
Formal. A block { ... } evaluates to the value of its final
expression when that expression has no trailing semicolon; with a
semicolon (or no final expression) the block's value is (). if,
match, and loop are expressions too, and a function's body block
provides its return value the same way.
In plain English. In Rust, most things that look like "do this" also produce a value. The last line of a block, without a semicolon, is the answer the block gives back.
Example. let grade = if score >= 50 { "pass" } else { "fail" };
stores a string. Adding a ; after "pass" makes that arm (), and the
compiler reports mismatched types.
Why this rule exists. It removes a whole category of temporary variables and makes the value of every branch explicit and checked.
Rule
A match must cover every possible value
also known as: exhaustive matching, E0004
Formal. A match expression must have an arm for every value its
scrutinee's type can hold; a missing case is error E0004, which names a
value that isn't covered. The wildcard pattern _ matches anything.
In plain English. When you match, the compiler checks that you
handled every possibility, and tells you exactly which one you forgot.
Example. match n { 0 => "zero", 1..=9 => "small" } on an i32 is
E0004 (negative numbers and 10 upward aren't covered). Adding
_ => "other" fixes it.
Why this rule exists. Forgetting a case is one of the most common bugs in branching code. Making it a compile error means adding a new case later shows you every place that must handle it.
Memory: the stack and the heap
Rule
A value is dropped when its owner goes out of scope
also known as: Drop, RAII, scope end
Formal. When the variable that owns a value goes out of scope, the
value is dropped: its Drop implementation (if any) runs, and any heap
memory it owns is freed. Variables in one scope are dropped in the
reverse order of their declaration. A value that has been moved out of a
variable is not dropped there.
In plain English. Rust cleans up for you at a predictable moment: the closing brace of the block that owns the thing. Nothing is left for a garbage collector to find later.
Example. In { let a = String::from("a"); let b = String::from("b"); },
b is dropped first, then a, both at the closing brace.
Why this rule exists. Knowing exactly when memory and other resources
(files, locks, connections) are released is what lets Rust manage them
without a garbage collector and without manual free calls.
Ownership
Rule
The ownership rules
also known as: single owner, ownership
Formal. (1) Every value has exactly one owner, a variable or another value that contains it. (2) Ownership can be transferred (moved), after which the old owner can no longer be used. (3) When the owner goes out of scope, the value is dropped.
In plain English. Each piece of data belongs to exactly one name at a time. You can hand it to someone else, but then it's theirs. When its owner is done, the data is cleaned up.
Example. let a = String::from("hi"); let b = a; moves the string to
b; using a afterwards is error E0382 ("borrow of moved value").
Why this rule exists. With exactly one owner, there is exactly one place responsible for freeing each value, so memory is never freed twice and never forgotten.
Rule
Assignment moves unless the type is Copy
also known as: move semantics, Copy trait, Clone
Formal. Assigning a value, passing it to a function, or returning it
moves it, unless its type implements Copy, in which case the bits are
duplicated and the source stays usable. Integers, floats, bool,
char, one more kind of value met in Chapter 8, and tuples/arrays of
Copy types are Copy;
String, Vec, and any type with a Drop implementation are not (a
type can't be both Copy and Drop). clone() is the explicit way to
duplicate a non-Copy value.
In plain English. Small, simple values get copied automatically.
Anything that owns memory somewhere else gets handed over instead, and if
you really want two, you ask for a copy with clone().
Example. let x = 5; let y = x; leaves both usable. let s = String::from("hi"); let t = s; leaves only t. let t = s.clone();
leaves both.
Why this rule exists. Copying a number is free; silently copying a large buffer is not. Making deep copies explicit keeps their cost visible, and moving keeps ownership single.
Borrowing and references
Rule
The borrowing rules
also known as: aliasing XOR mutation, E0499, E0502
Formal. At any point in a program, a value may have either any
number of shared references (&T) or exactly one mutable reference
(&mut T), but not both; and no reference may be used after the value
it refers to is gone. Violations are compile errors (E0499 for two
mutable borrows, E0502 for a mutable borrow alongside a shared one).
In plain English. Many people may read something at once, or one person may change it, never both at the same time. And you can't keep a pointer to something that has been thrown away.
Example. let r = &v; v.push(4); println!("{r:?}"); is E0502:
push needs to change v while r is still reading it (and push may
move the vector's contents, leaving r pointing at freed memory).
Why this rule exists. Reading and writing the same data at the same time is the root of data races and use-after-free bugs. Ruling it out at compile time removes those bugs from safe Rust.
Rule
The three ingredients of a data race
also known as: data race, race condition
Formal. A data race happens when (1) two or more accesses to the same memory occur at the same time, (2) at least one of them writes, and (3) nothing synchronizes them. Its outcome is undefined. Remove any one ingredient and there is no data race.
In plain English. Trouble needs all three: two things touching the same data at once, one of them changing it, and nothing making them take turns.
Example. The borrowing rules remove ingredient (2) whenever there is more than one reference (many readers, no writer) and ingredient (1) whenever there is a writer (only one reference exists).
Why this rule exists. Data races produce bugs that appear only sometimes and are very hard to reproduce. Knowing the three ingredients explains exactly what Rust's rules are preventing.
Rule
A borrow lasts until its last use
also known as: NLL
Formal. The compiler considers a reference alive from where it is created to the last place it is used, not to the end of the enclosing block. A conflicting borrow that starts after that last use is allowed.
In plain English. Once you've finished using a reference, Rust treats it as returned, even if its name is still in scope.
Example. let r = &v; println!("{r:?}"); v.push(4); compiles:
r's last use is the println!, so the mutable borrow for push
doesn't overlap it. Moving the println! after push makes it E0502.
Why this rule exists. Tying borrows to actual use rather than to braces lets many correct programs compile without contortions, while still rejecting every real overlap.
Rule
A reference can't outlive the value it refers to
also known as: dangling reference, E0597, E0106
Formal. Every reference must remain within the lifetime of its referent; code in which a reference could be used after its referent is dropped does not compile (E0597, "borrowed value does not live long enough"). A function can't return a reference to one of its own local variables.
In plain English. You can't hold on to a pointer to something that has already been cleaned up. Rust checks this before the program runs.
Example. let r; { let x = 5; r = &x; } println!("{r}"); is E0597:
x is dropped at the inner closing brace while r still refers to it.
Why this rule exists. Using memory after it has been freed is one of the most dangerous bugs in systems code. Rust makes it a compile error.
Enums and pattern matching
Rule
A match must cover every possible value
also known as: exhaustive matching, E0004
Formal. A match expression must have an arm for every value its
scrutinee's type can hold; a missing case is error E0004, which names a
value that isn't covered. The wildcard pattern _ matches anything.
In plain English. When you match, the compiler checks that you
handled every possibility, and tells you exactly which one you forgot.
Example. match n { 0 => "zero", 1..=9 => "small" } on an i32 is
E0004 (negative numbers and 10 upward aren't covered). Adding
_ => "other" fixes it.
Why this rule exists. Forgetting a case is one of the most common bugs in branching code. Making it a compile error means adding a new case later shows you every place that must handle it.
Rule
Refutable and irrefutable patterns
also known as: refutability, E0005
Formal. A pattern is irrefutable if it matches every possible value
of its type (x, (a, b), Point { x, y }) and refutable if some value
fails to match (Some(x), 0..=9). let, function parameters, and for
require irrefutable patterns (a refutable one in let is E0005 unless it
has an else); if let, while let, let ... else, and match arms
accept refutable ones.
In plain English. Some patterns can't fail, like giving a name to a value. Others can, like "if it's a Some". Places that have nowhere to go on failure only accept the first kind.
Example. let Some(n) = maybe_number; is E0005.
let Some(n) = maybe_number else { return }; compiles, because it says
what to do when the pattern fails.
Why this rule exists. It guarantees every pattern either always matches or comes with a plan for when it doesn't.
Handling errors
Rule
What the ? operator does
also known as: question mark operator, error propagation
Formal. For a Result, expr? evaluates to the Ok value, or
returns early from the enclosing function with the Err value, first
converting it with From::from into the function's error type. For an
Option, it evaluates to the Some value or returns None. It may only
be used in a function (or closure) whose return type is a compatible
Result or Option.
In plain English. The question mark means "if this worked, give me the value and keep going; if it failed, stop here and pass the failure back to whoever called me."
Example. let text = std::fs::read_to_string(path)?; inside a
function returning Result<String, std::io::Error> either gives text
or returns the I/O error. Inside a function returning () it is error
E0277.
Why this rule exists. It keeps error handling visible (every
fallible call is marked) without burying the program's logic in
match boilerplate.
Collections
Rule
A push can move a Vec's elements
also known as: reallocation invalidates references, E0502
Formal. A Vec<T> keeps its elements in one heap buffer of some
capacity. When a push (or insert, extend, reserve) needs more room
than the capacity, the vector allocates a larger buffer, moves every
element into it, and frees the old one. Because a push takes &mut self,
the borrowing rules forbid it while any reference into the vector is
still in use (E0502), so no reference can be left pointing into the freed
buffer.
In plain English. A vector's items live together in one block of memory. When the block is full, adding one more means moving everything to a bigger block. If you were still holding onto an item's old spot, you'd be holding onto nothing, so Rust won't let you add while you hold it.
Example. let first = &v[0]; v.push(4); println!("{first}"); is
rejected with E0502. Printing first before the push, or copying the
value out with let first = v[0];, compiles.
Why this rule exists. In C++ the same code compiles and reads freed memory after a reallocation, a classic use-after-free bug.
Strings, text, and Unicode
Rule
A str is always valid UTF-8
also known as: char boundaries, byte index is not a char boundary
Formal. Every String and str holds valid UTF-8, and every safe
operation preserves that. Indexing a string by an integer is not
defined (E0277). A range slice &s[a..b] is allowed only when a and
b are char boundaries with a <= b <= s.len(); otherwise it panics.
s.get(a..b) returns None instead.
In plain English. Rust promises a string is always proper text, never
half a character. You can cut a string only between characters; cutting
through the middle of a character stops the program, unless you ask with
get, which answers "no" instead.
Example. For let s = "héllo";, &s[0..1] is "h", &s[0..3] is
"hé", and &s[0..2] panics because byte 2 is inside é, which takes
bytes 1 and 2.
Why this rule exists. If a slice could split a character, every function that reads strings would have to cope with broken text. Checking at the slice keeps every other function simple and correct.
Generics
Rule
Generic code is compiled once per type
also known as: monomorphization, static dispatch, zero-cost generics
Formal. For each distinct set of concrete type (and const) arguments a generic function or type is used with, the compiler generates a separate specialized copy. Calls to it are resolved at compile time (static dispatch) and may be inlined, so generic code runs as fast as code written by hand for each type, at the cost of compile time and binary size.
In plain English. Writing a function once for "any type T" doesn't
make the program look up anything while it runs. The compiler quietly
writes one version for i32, another for f64, and so on, as if you had
typed them all.
Example. Calling count_above(&[1, 2, 3], &2) and
count_above(&[1.5, 0.5], &1.0) produces two machine-code functions,
count_above::<i32> and count_above::<f64>.
Why this rule exists. It gives Rust abstractions without a runtime price, which is what lets high-level code be used where speed matters.
Traits
Rule
A supertrait is a requirement, not a parent
also known as: supertraits, trait B: A
Formal. trait B: A { .. } declares A a supertrait of B: a type
may implement B only if it also implements A (E0277 otherwise), and
code that knows T: B may call A's methods on T. Each trait is still
implemented in its own impl block. Nothing is inherited: no fields, no
method bodies, no overriding.
In plain English. "To be a B, you must already be an A." It's an entry requirement, like needing a driving licence before a taxi licence. The taxi licence doesn't give you the driving skills; you still had to earn both.
Example. With trait Named { fn name(&self) -> String; } and
trait Greeter: Named { fn greet(&self) -> String { format!("Hi, {}", self.name()) } },
a type needs both impl Named and impl Greeter.
Why this rule exists. It lets a trait's default methods use another trait's methods without the tangled hierarchies inheritance creates.
Rule
The orphan rule
also known as: coherence, E0117, newtype workaround
Formal. An impl Trait for Type is allowed only in a crate that
defines Trait or defines Type (with some allowance for local types
appearing as type arguments). Implementing a foreign trait for a foreign
type is error E0117. Together with the ban on overlapping impls, this
guarantees that at most one impl of a trait exists for any type across
every crate in a program (coherence).
In plain English. You can add your own abilities to anybody's types, or teach your own types anybody's abilities, but you can't bolt someone else's ability onto someone else's type. Otherwise two libraries could each do it differently and nobody could tell which one wins.
Example. impl fmt::Display for Vec<String> is rejected. Wrapping it,
struct Lines(Vec<String>); and impl fmt::Display for Lines, compiles.
Why this rule exists. Adding a dependency should never break a program by bringing in a clashing impl.
Trait objects and dispatch
Rule
An impl Trait return type is one hidden type
also known as: opaque type, return-position impl Trait
Formal. A function declared -> impl Trait returns values of exactly
one concrete type, chosen by the function body and hidden from callers,
who may use only Trait (and auto traits such as Send that the hidden
type has). Returning different concrete types from different branches is
a type mismatch (E0308). In edition 2024 the hidden type may capture
every generic parameter and lifetime in scope unless a use<..> bound
lists fewer.
In plain English. "I'll give you something that can do X. I won't tell you what it is, but it's always the same kind of thing." If you need to hand back different kinds of things, you need a box that can hold any of them.
Example. fn evens() -> impl Iterator<Item = u32> { (0..).step_by(2) }
compiles; an if returning (0..3) in one branch and vec![1].into_iter()
in the other does not.
Why this rule exists. One concrete type keeps the call statically dispatched and allocation-free, while still letting the function change its internal type without breaking callers.
Rule
Which traits can be trait objects
also known as: dyn compatibility, object safety, E0038
Formal. A trait can be used as dyn Trait only if it is dyn
compatible: it has no Sized supertrait, no associated constants, and
every method that isn't marked where Self: Sized takes a receiver
(&self, &mut self, Box<Self>, ...), has no type parameters, and
does not use Self except as that receiver. Using an incompatible trait
as dyn Trait is error E0038.
In plain English. A trait object only knows "some type that has these methods," reached through a table of function pointers. A method that needs to know the real type (to return one, or to be copied out for every possible generic type) can't be put in that table.
Example. Clone isn't dyn compatible, because clone returns
Self; Display is, because fmt(&self, ..) only borrows.
Why this rule exists. Every method of a trait object must be callable through its vtable without knowing the concrete type.
Designing errors
Rule
What the ? operator does
also known as: question mark operator, error propagation
Formal. For a Result, expr? evaluates to the Ok value, or
returns early from the enclosing function with the Err value, first
converting it with From::from into the function's error type. For an
Option, it evaluates to the Some value or returns None. It may only
be used in a function (or closure) whose return type is a compatible
Result or Option.
In plain English. The question mark means "if this worked, give me the value and keep going; if it failed, stop here and pass the failure back to whoever called me."
Example. let text = std::fs::read_to_string(path)?; inside a
function returning Result<String, std::io::Error> either gives text
or returns the I/O error. Inside a function returning () it is error
E0277.
Why this rule exists. It keeps error handling visible (every
fallible call is marked) without burying the program's logic in
match boilerplate.
Closures
Rule
How a closure captures a variable
also known as: capture modes
Formal. For each variable from its surroundings that a closure's body
uses, the compiler picks the least powerful capture that works: a shared
reference if the body only reads it, a mutable reference if the body
changes it, and ownership if the body moves it out (for example, returns
it or passes it by value). The move keyword makes every capture by
value instead. The capture modes decide which of the three closure
traits (section 4) the closure implements, and the borrowing rules apply to the
captures for as long as the closure is alive.
In plain English. A closure takes only what it needs. If it only
looks at a variable, it borrows it; if it changes it, it borrows it
exclusively; if it gives it away, it has to own it. move says "take
everything with you."
Example. let mut count = 0; let mut tick = || count += 1; borrows
count mutably, so println!("{count}") between two calls of tick()
is error E0502; after the last call of tick() it compiles.
Why this rule exists. Capturing as little as possible keeps the surrounding code usable, while the borrow checker still sees every capture and can prove the closure never touches freed or aliased data.
Iterators
Rule
Iterators are lazy
also known as: laziness, unused_must_use
Formal. Methods like map, filter, zip and take return a new
iterator and do no work when they are called. Work happens only when
something calls next: a for loop or a finishing method (collect,
sum, count, fold, find, ...). Each call to next pulls one item
through the whole chain, and a finishing method that has its answer
(find, any, or a take that is full) stops pulling. A chain that is
never finished triggers the unused_must_use warning.
In plain English. Writing .map(...) is writing down a recipe, not
cooking. Nothing is cooked until someone asks for a plate, and each plate
goes through every step before the next one starts.
Example. (1..).map(|n| n * n).filter(|n| n % 2 == 1).take(3) is
fine even though 1.. never ends: collect pulls exactly the items
needed for 1, 9 and 25, then stops.
Why this rule exists. Laziness lets a chain skip work it doesn't need, avoid building intermediate collections, and describe infinite sequences, while compiling to the same code as a hand-written loop.
Lifetimes
Rule
Lifetimes describe, they never extend
also known as: 'a, E0597, E0515
Formal. A lifetime is a region of the program during which a
reference must be valid. Names like 'a in a signature state
relationships between the regions of inputs and outputs, and the
compiler checks every call against them. They never change when any
value is dropped: a value still lives exactly until the end of its
owner's scope (or until it is moved or consumed), and a reference that
would outlive it is rejected (E0597, E0515) no matter how it is
annotated.
In plain English. A lifetime label is a promise written on a reference, "this stays good at least as long as that," and the compiler checks the promise. Writing the label can't make anything last longer, the way writing a later date on a carton doesn't keep the milk fresh.
Example. fn shorter<'a>(x: &'a str, y: &'a str) -> &'a str says the
result is valid only while both arguments are. Calling it with a string
that is dropped at the end of an inner block and then using the result
after that block is error E0597.
Why this rule exists. Annotations let the compiler check each function on its own, from its signature, without looking into the bodies of the functions it calls.
Rule
The lifetime elision rules
also known as: elision, E0106
Formal. In a function signature, a reference type written without a
lifetime gets one by three rules, which look only at the signature:
(1) each elided lifetime in the parameters becomes its own distinct
lifetime parameter; (2) if there is exactly one input lifetime, it is
assigned to every elided output lifetime; (3) if there are several input
lifetimes but one of them is &self or &mut self, self's lifetime is
assigned to every elided output lifetime. If an output lifetime is still
unassigned, the signature is error E0106 and must be annotated.
In plain English. Most of the time Rust can guess how a returned reference is tied to the inputs: one reference in, so the result must come from it; a method, so the result probably comes from the object. When there's more than one possibility, you have to say which.
Example. fn first_word(s: &str) -> &str means
fn first_word<'a>(s: &'a str) -> &'a str (rule 2).
fn pick(x: &str, y: &str) -> &str is E0106: two inputs, no self.
Why this rule exists. Writing lifetimes out every time is noise for the common cases, which the rules cover, while ambiguous cases still have to be spelled out, so no guess is ever silently wrong.
Modules and project structure
Rule
Rust's privacy rules
also known as: pub, pub(crate), E0603, E0616
Formal. Every item (function, type, constant, module, field, method)
is private by default: it can be used in the module that defines it and
in that module's descendants, and nowhere else. pub makes it visible
wherever its parent module is visible; pub(crate) anywhere in the same
crate; pub(super) in the parent module; pub(in path) inside the named
ancestor. A path is usable only if every module along it is visible from
the place it is written. Struct fields have their own visibility, so a
pub struct can keep every field private. Using an item that is not
visible is an error (E0603 for an item, E0616 for a field, E0624 for a
method).
In plain English. Everything starts locked inside the room it was
made in. Rooms inside that room can see in; nobody else can. pub
unlocks the door to whoever can already reach the room.
Example. Inside mod shop { pub struct Item { price: u32 } }, code
outside shop can name shop::Item but can't read item.price
(E0616) or build Item { price: 3 } (E0451).
Why this rule exists. Privacy lets a module keep promises: if only its own code can touch a field, only its own code can break the rules that field must follow, and the rest of the program can change freely around it.
Cargo in depth
Rule
How Cargo reads a version requirement
also known as: caret requirements, semver, ^1.4
Formal. A dependency written as name = "1.4" means the caret
requirement ^1.4: any version >= 1.4.0 and < 2.0.0. Below 1.0 the
first non-zero component counts as the major version, so "0.4" means
>= 0.4.0, < 0.5.0. Cargo resolves each requirement to the newest
published version that satisfies it (and, with resolver 3, fits the
package's rust-version when possible), records the choice in
Cargo.lock, and keeps using it until cargo update. Versions that are
semver-incompatible (1.x and 2.x) can both appear in one build as
separate crates.
In plain English. "1.4" doesn't mean exactly 1.4. It means "1.4 or any later 1.something," because those promise not to break your code. Cargo picks the newest such version once and writes it down so every build uses the same one.
Example. With rand = "0.8" and a published 0.8.5 and 0.9.0, Cargo
picks 0.8.5; 0.9.0 is a different major version for a 0.x crate.
Why this rule exists. It lets you get bug fixes automatically while never being moved to a release that is allowed to break you.
Rule
Cargo features are additive and unified
also known as: features, feature unification
Formal. When several packages in one build depend on the same crate (at the same semver-compatible version), Cargo builds that crate once, with the union of every feature any of them requested. A feature therefore must only add functionality (new items, optional dependencies, extra impls); enabling it must never remove an item or change existing behavior in a way that breaks code written without it.
In plain English. If one part of your program switches a feature on, it is on for everyone who uses that crate. So a feature should only ever add things, like an optional extra, never take something away.
Example. If your app uses serde without derive and a dependency
enables serde/derive, your app's serde has derive too.
Why this rule exists. Building one copy of each crate keeps builds small and types compatible across the dependency graph; that only works if turning a feature on can't break anyone.
Testing
Rule
Integration tests see only the public API
also known as: tests/ folder, integration tests
Formal. Each .rs file directly inside a package's tests/ folder
is compiled by cargo test as a separate crate that depends on the
package's library crate. It can therefore use only items reachable
through the library's public API, exactly as another crate could.
Binary crates (src/main.rs) cannot be imported by integration tests
at all, which is one reason to put logic in src/lib.rs.
In plain English. A test in the tests/ folder is an outsider. It uses your library the same way a stranger would, through the front door, so it checks what users will really experience.
Example. If src/lib.rs has a private fn normalize, a unit test
inside src/lib.rs can call it, but tests/api.rs gets E0603.
Why this rule exists. Testing through the public API catches mistakes in what users actually see, and it lets the internals change without rewriting the tests.
Files and the filesystem
Rule
File names are not always strings
also known as: OsStr, Path, to_string_lossy
Formal. Operating systems store file names, environment variables,
and command-line arguments as platform strings: arbitrary non-zero
bytes on Unix, and 16-bit units (possibly unpaired surrogates) on
Windows. Neither is guaranteed to be valid UTF-8, so Rust gives them
their own types, OsString/OsStr and PathBuf/Path. Converting to
&str can fail (to_str returns Option), to_string_lossy replaces
bad sequences with U+FFFD, and std::env::args panics on an argument
that isn't Unicode (args_os doesn't).
In plain English. A file name is whatever the operating system allows, and that isn't always proper text. Rust keeps paths in a separate type so your program can still handle the odd ones, and asks you to decide what to do when you need them as text.
Example. Path::new("notes").join("2026").join("todo.txt") builds
notes/2026/todo.txt on macOS and Linux and notes\2026\todo.txt on
Windows; path.display() prints it for people.
Why this rule exists. Treating paths as strings silently breaks on real systems: files a program can list but not open, or names that get mangled when written back.
Processes, environment, and I/O
Rule
Standard output is data, standard error is for people
also known as: stdout, stderr, eprintln!, exit status
Formal. A process has three standard streams: standard input,
standard output (println!, io::stdout()), and standard error
(eprintln!, io::stderr()). Shells redirect and pipe standard output
(>, |) while standard error still goes to the terminal, and the
process's exit status (0 for success, non-zero for failure) is how
scripts and other programs learn whether it worked. io::stdout() is
line-buffered; lock it (stdout().lock()) or wrap it in a BufWriter
for heavy output.
In plain English. Your program's results go out one door and its complaints go out another, so someone saving the results to a file still sees the complaints on screen. The exit number is the one-word summary: 0 means "it worked".
Example. mytool > out.txt puts println! lines in the file while
an eprintln!("warning: ...") still appears in the terminal.
Why this rule exists. Command-line programs are joined together by pipes and scripts; mixing messages into the data, or reporting failure with status 0, breaks every program downstream.
Smart pointers
Rule
RefCell checks the borrowing rules at run time
also known as: interior mutability, RefCell already borrowed
Formal. RefCell<T> allows mutation through a shared reference by
tracking borrows at run time: borrow() hands out any number of shared
guards and borrow_mut() one exclusive guard, and each guard releases
its borrow when dropped. Asking for a borrow that would break the rules
(an exclusive one while any other exists, or a shared one while an
exclusive one exists) panics ("RefCell already borrowed");
try_borrow/try_borrow_mut return an error instead. RefCell is not
Sync, so it is single-threaded only.
In plain English. The same rule as always, many readers or one writer, but checked while the program runs instead of when it compiles. Break it and the program stops with a panic instead of a compile error.
Example. let log = RefCell::new(Vec::new()); let a = log.borrow(); log.borrow_mut().push(1); panics, because a is still reading.
Why this rule exists. Some designs (a graph, a test fake that records
calls through &self) need shared data that changes; RefCell keeps them
memory-safe when the compiler can't prove the borrows statically.
Rule
Leaking memory is safe in Rust
also known as: reference cycles, mem::forget, Box::leak
Formal. Rust's safety guarantee rules out use-after-free, double
free, and data races, but not memory leaks: safe code may make a value
unreachable without dropping it (std::mem::forget, Box::leak, an
Rc cycle). Such a value's Drop never runs and its memory is never
freed. Consequently unsafe code must never rely on a destructor running
for soundness.
In plain English. Rust promises you'll never use memory after it's
freed. It doesn't promise every piece of memory will be freed. Two Rc
values holding on to each other will just sit there forever, and that's
allowed.
Example. Two Rc<RefCell<Node>> whose next fields point at each
other keep both strong counts at 1 after the last outside Rc is
dropped; changing one link to Weak lets both be freed.
Why this rule exists. Preventing every leak would forbid useful
tools (reference counting, forget) or make them unsafe; a leak wastes
memory but can't corrupt it.
Threads and message passing
Rule
Send and Sync
also known as: thread safety, E0277 cannot be sent between threads safely
Formal. A type is Send if a value of it can be moved to another
thread, and Sync if a shared reference &T can be used from several
threads at once (equivalently, &T: Send). Both are auto traits: a
struct is Send/Sync exactly when all its fields are. thread::spawn
requires its closure (and the value it returns) to be Send + 'static;
violating that is a compile error (E0277). Rc and RefCell are not
Sync, and Rc is not Send; Arc<T> is Send + Sync when T is
Send + Sync, and the lock types of the next chapter make shared data Sync.
In plain English. The compiler labels every type "safe to hand to another thread" and "safe to share between threads", works the labels out from what the type contains, and refuses to compile a program that would move or share something without the right label.
Example. Spawning a thread with a move closure that captures an
Rc<Vec<u32>> is E0277; with an Arc<Vec<u32>> it compiles.
Why this rule exists. Data races are undefined behavior; checking thread safety in the type system turns them into compile errors instead of rare, unreproducible bugs.
Async fundamentals
Rule
Futures are lazy
also known as: async fn, .await, polling
Formal. Calling an async fn (or evaluating an async block)
creates a future and runs none of its body. The body runs only when the
future is polled: when it is .awaited inside another async context, or
handed to Tokio to run (tokio::spawn, block_on, #[tokio::main]).
A future that is dropped without being polled never runs; one dropped
partway is cancelled at its last .await. The compiler warns about an
unused future (unused_must_use).
In plain English. An async function call gives you a to-do note, not the work. Nothing happens until someone waits for it or hands it to the runtime to do, and throwing the note away means the work never happens.
Example. let f = fetch_price("AAPL"); prints nothing; the
function's first println! appears only at f.await.
Why this rule exists. Laziness means futures cost nothing until used and can be combined, raced, or cancelled freely; it's also the biggest difference from JavaScript promises, which start as soon as they're created.
Networking
Rule
TCP is a byte stream, not a message queue
also known as: framing, read_line, length prefix
Formal. A TCP connection delivers the bytes written at one end to
the other end reliably and in order, but it keeps no record of where one
write ended and the next began. A single read may return part of a
write, or the end of one write joined to the start of the next, and
returns Ok(0) only when the peer has closed its side. Any protocol on
top of TCP must therefore define its own framing: a delimiter (such as a
newline, read with BufRead::read_line), a length prefix, or a
self-describing format.
In plain English. TCP is a pipe for bytes, not an envelope service. What you wrote in three pieces may come out in one lump or in ten crumbs, so both sides have to agree how to tell where each message ends.
Example. A client that sends "PING\n" and "PING\n" in two writes
may have the server's single read return "PING\nPI"; reading lines
with a BufReader gets exactly two "PING" lines every time.
Why this rule exists. Networks split and join packets as they see fit; hiding that behind a reliable stream is what TCP is for, and message boundaries are the price.
Web backends
Rule
Store passwords as slow salted hashes
also known as: Argon2id, password hashing, never roll your own crypto
Formal. A service must never store passwords or reversibly encrypted passwords. It stores the output of a password-hashing function that is salted (a unique random value per password, stored with the hash) and deliberately slow and memory-hard (Argon2id is the current recommendation; bcrypt and scrypt are older alternatives). Logging in recomputes the hash from the typed password and the stored salt and compares. General-purpose hashes such as SHA-256 are too fast for this.
In plain English. Keep a fingerprint of each password, not the password, and make each fingerprint slow to compute and different for every user even when two people chose the same password. A thief who copies the database then has to guess, slowly, one person at a time.
Example. With the argon2 crate (0.6), Argon2::default().hash_password(pw.as_bytes())
makes a fresh random salt and returns a PHC string like $argon2id$v=19$m=19456,t=2,p=1$...
that already contains the salt and settings; verify_password checks a login attempt against
the stored string (PasswordHash lives at password_hash::phc::PasswordHash).
Why this rule exists. Databases leak. Slow, salted hashes turn a leak from "every password exposed" into "each guess costs real time and money".
Databases
Rule
Bind values, never format them into SQL
also known as: prepared statements, SQL injection, .bind()
Formal. A query's text and its values travel separately: the SQL
contains placeholders (? for SQLite and MySQL, $1, $2, ... for
PostgreSQL) and each value is passed with .bind(value). The database
parses the SQL before it sees any value, so a value can never change the
query's structure. Building SQL with format! or string concatenation
from input lets that input become SQL (SQL injection).
In plain English. Write the question once with blanks in it, and hand the answers over in a separate envelope. Then no answer, however strange, can rewrite the question.
Example. sqlx::query("SELECT id FROM users WHERE email = ?").bind(email)
treats email = "x' OR '1'='1" as an odd email address; the same text
spliced in with format! returns every user.
Why this rule exists. Injection has been one of the most common and damaging web vulnerabilities for decades, and binding eliminates it completely for values.
Unsafe Rust
Rule
What unsafe allows
also known as: unsafe block, the five superpowers
Formal. Inside an unsafe block (or an unsafe impl), Rust permits
exactly five operations it otherwise forbids: dereferencing a raw
pointer, calling an unsafe function (including foreign functions),
implementing an unsafe trait, accessing or modifying a mutable
static, and reading a field of a union. Every other rule still
applies: types are checked, references are borrow-checked, and lifetimes
are enforced. What changes is who is responsible: the compiler can no
longer prove the five operations are valid, so the programmer must,
and undefined behavior results if they are wrong.
In plain English. unsafe doesn't switch the safety checks off. It
unlocks five specific tools the compiler can't check, and it means "I
have checked these myself." If you're wrong, the program can break in
ways Rust normally rules out.
Example. let p = &x as *const i32; is safe; unsafe { *p } is the
unsafe part, correct only because x is still alive and aligned.
Why this rule exists. Some things (hardware access, foreign code, low-level data structures) can't be checked by the compiler; confining them to small marked blocks tells reviewers exactly where to look.
Foreign function interface
Rule
Panics must not unwind across an FFI boundary
also known as: catch_unwind, extern "C", abort on unwind
Formal. An extern "C" function must not let a Rust panic unwind out
of it into foreign code: since Rust 1.81 such an unwind aborts the
process. Functions called from C should catch panics at the boundary
with std::panic::catch_unwind and translate them into an error value
the caller understands (an error code, a null pointer, an out-parameter).
The extern "C-unwind" ABI exists for the rare cases where unwinding
across the boundary is intended and both sides support it.
In plain English. C has no idea what a Rust panic is. If one escapes into C, the whole program stops. So catch it at the door and hand C an error code it does understand.
Example. #[unsafe(no_mangle)] pub extern "C" fn parse(s: *const c_char) -> i32 { match std::panic::catch_unwind(|| ...) { Ok(n) => n, Err(_) => -1 } }.
Why this rule exists. Unwinding through frames compiled by another language skips their cleanup and corrupts their state; aborting is the only safe default.
Performance
Rule
Measure before you optimize
also known as: release builds, benchmarks
Formal. Performance claims are established by measurement of an
optimized (--release) build on representative input: a benchmark
harness (criterion, cargo bench) for small functions, and a tool that
samples where a whole program spends its time. An optimization is kept only if a repeated measurement
shows an improvement larger than the run-to-run noise, and the tests
still pass. Debug-build timings are not evidence.
In plain English. Don't guess where the program is slow; find out. Time the fast build, change one thing, time it again, and keep the change only if it clearly helped and nothing broke.
Example. A sampling run shows 70% of the time in one parsing function;
replacing a per-line String allocation there with a reused buffer cuts
the run time, while "optimizing" a function at 2% changes nothing
measurable.
Why this rule exists. Most code isn't on the hot path, and changes made on hunches add complexity without making anything faster.
Idiomatic Rust
Rule
Parse, don't validate
also known as: newtype validation, TryFrom, serde try_from
Formal. Check untrusted data once, at the boundary, by converting it
into a type whose values are valid by construction (a newtype with a
private field and a fallible constructor or TryFrom impl, an enum
instead of a string, NonZeroU32 instead of u32). Code that receives
the type can rely on its invariant without checking again. With serde,
#[serde(try_from = "Raw")] or a custom Deserialize makes
deserialization itself perform the conversion, so invalid input becomes
a deserialization error.
In plain English. Instead of checking a value and then carrying it around as a plain string or number (and hoping everyone remembers it was checked), turn it into a special type that can only ever hold good values. The check happens once, and the type remembers it.
Example. struct Port(u16) with impl TryFrom<u16> for Port that
rejects 0: a function taking Port never has to ask whether it's zero.
Why this rule exists. Scattered checks get forgotten; a type that can't represent bad data makes the compiler enforce the check everywhere.
Rule
Make illegal states unrepresentable
also known as: smart constructors, parse don't validate, typestate
Formal. Design types so that every value a type can hold is a valid
one: use enums for mutually exclusive cases (instead of several
Options or flags that must agree), newtypes with private fields and
validating constructors for constrained values, and typestate for
ordering constraints. Code that receives such a type needs no defensive
checks, because the compiler rejects any construction that could
produce an invalid value.
In plain English. If a combination of fields should never happen, don't let the type express it. Then no function anywhere has to wonder whether it's holding something broken.
Example. enum Delivery { Pending, Shipped { tracking: TrackingId } }
can't hold "shipped with no tracking number", while struct Delivery { shipped: bool, tracking: Option<String> } can.
Why this rule exists. Invalid states that can't be written down can't turn into bugs, and the checks happen once, at the edge.
Observability and security
Rule
Memory safety is not the same as security
also known as: threat model, what Rust guarantees
Formal. Safe Rust guarantees the absence of undefined behavior:
no use-after-free, double free, out-of-bounds access, data races, or
reads of uninitialized memory in code without unsafe. It guarantees
nothing about a program's logic: injection (SQL, command, path),
broken authentication or authorization, leaked secrets, unbounded
resource use (denial of service), unsafe deserialization of trusted
fields, vulnerable dependencies, and bugs inside unsafe code are all
still possible and must be prevented by design, review, and testing.
In plain English. Rust removes one big family of bugs, the ones where a program scribbles on memory it shouldn't. A Rust program can still do exactly the wrong thing, carefully and correctly: let the wrong person in, run a query an attacker wrote, or run out of memory because someone sent a huge request.
Example. An axum handler that formats a query string into SQL is memory-safe and still vulnerable to SQL injection; binding the value fixes the security bug, not a memory bug.
Why this rule exists. Treating "written in Rust" as "secure" skips the threat modeling every service needs.
Rule
Parse, don't validate
also known as: newtype validation, TryFrom, serde try_from
Formal. Check untrusted data once, at the boundary, by converting it
into a type whose values are valid by construction (a newtype with a
private field and a fallible constructor or TryFrom impl, an enum
instead of a string, NonZeroU32 instead of u32). Code that receives
the type can rely on its invariant without checking again. With serde,
#[serde(try_from = "Raw")] or a custom Deserialize makes
deserialization itself perform the conversion, so invalid input becomes
a deserialization error.
In plain English. Instead of checking a value and then carrying it around as a plain string or number (and hoping everyone remembers it was checked), turn it into a special type that can only ever hold good values. The check happens once, and the type remembers it.
Example. struct Port(u16) with impl TryFrom<u16> for Port that
rejects 0: a function taking Port never has to ask whether it's zero.
Why this rule exists. Scattered checks get forgotten; a type that can't represent bad data makes the compiler enforce the check everywhere.
Capstone: a production service
Rule
Make illegal states unrepresentable
also known as: smart constructors, parse don't validate, typestate
Formal. Design types so that every value a type can hold is a valid
one: use enums for mutually exclusive cases (instead of several
Options or flags that must agree), newtypes with private fields and
validating constructors for constrained values, and typestate for
ordering constraints. Code that receives such a type needs no defensive
checks, because the compiler rejects any construction that could
produce an invalid value.
In plain English. If a combination of fields should never happen, don't let the type express it. Then no function anywhere has to wonder whether it's holding something broken.
Example. enum Delivery { Pending, Shipped { tracking: TrackingId } }
can't hold "shipped with no tracking number", while struct Delivery { shipped: bool, tracking: Option<String> } can.
Why this rule exists. Invalid states that can't be written down can't turn into bugs, and the checks happen once, at the edge.
Rule
as never fails: it truncates, wraps, or saturates
also known as: numeric cast, as vs TryFrom
Formal. A numeric as cast always produces a value. Integer to
smaller integer keeps the low bits; float to integer rounds toward zero
and saturates at the target's limits (NaN becomes 0). From is provided
only where no information can be lost, and TryFrom/try_into return a
Result for conversions that can fail.
In plain English. as is the "just do it" conversion: it never
complains, even when it has to throw information away. Use from when
nothing can be lost, and try_from when it might be, so you find out.
Example. 300_i32 as u8 is 44; u8::try_from(300_i32) is an
Err; u32::from(200_u8) is 200.
Why this rule exists. Low-level code sometimes needs the raw bit behavior, but ordinary code should be told when a conversion loses data.
Appendix A: Old way, modern way
Rule
Editions are chosen per crate
also known as: edition = "2024", Rust editions
Formal. Each crate declares one edition in its Cargo.toml
(edition = "2024"). The edition decides which syntax and lint defaults
the compiler applies to that crate's own code. Crates of different
editions link together freely, and every new stable compiler still
builds every older edition.
In plain English. An edition is a setting a project chooses, not a separate version of Rust you install. Your project can use the 2024 rules while a library it depends on still uses 2021, and they work together.
Example. Let chains (if let Some(x) = a && x > 0) compile in a
crate with edition = "2024" and are rejected in one with
edition = "2021", on the same compiler.
Why this rule exists. It lets the language fix old mistakes without breaking the millions of lines of code already written.