Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

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.