Skip to content
teach

Learning: Rust

Become the engineer trusted to own Rust on a team: able to design with ownership rather than negotiating with the borrow checker, shape errors and APIs so the types carry the invariants, reach for unsafe only behind a boundary that can be justified, and ship a crate other people depend on and can upgrade.

Start here: 0001. Ownership and Drop
Latest lesson: 0067. Property-Based and Snapshot Testing

Success looks like

  • Predict which borrow the compiler will reject, and why, before compiling.
  • Choose between owning, borrowing, reference counting and interior mutability from how long the data actually has to live.
  • Design an error type callers can handle, and say when panicking is the correct choice instead.
  • Remove duplication with traits and generics without making the signatures unreadable, and know when dynamic dispatch is the better answer.
  • Write async code that does not stall, and explain what holding a lock across an await point does.
  • Justify every unsafe block by the invariant it upholds, keep it behind a safe interface, and check it with a tool rather than by reading.
  • Publish a crate with documentation, tests, and a public API you can evolve without breaking dependants.
  • Review Rust and name precisely why a lifetime annotation, a clone, or a shared mutex is covering for a design problem.

Constraints

  • Assumes no prior Rust and no systems-programming background. The stack, the heap and memory layout are taught inside the arc, because ownership is unteachable without them.
  • Needs only the stable toolchain and a terminal on any supported OS. Nothing in the arc requires paid tooling, a cloud account, or nightly Rust.
  • Reps are expected to fail to compile. That is the feedback loop, not a setback: predict what the compiler will say, then find out, and treat the difference as the lesson.
  • Reps are small programs that fit one sitting. Spacing them across days is the mechanism, not an inconvenience.
  • Editions and releases matter here. Where a lesson depends on an edition or a stabilised feature, it says which, and version-sensitive claims are checked against the release notes rather than recalled.

Out of scope

  • Embedded targets and no_std as subjects in their own right.
  • WebAssembly, graphical interfaces, and game engines.
  • Web frameworks as subjects. Async needs examples, and they stay as small as the concept allows.
  • Procedural macro authoring. Declarative macros appear where the standard library's own use of them has to be read.
  • Foreign function interfaces past what the unsafe boundary material needs. Wrapping a real C library is a separate undertaking.
  • Compiler and borrow-checker internals past the point where they stop predicting what the compiler will accept.

The arc

Eight stages, zero to senior. Not a lesson list: a stage takes several lessons, and the boundaries are soft.

Stage Lessons Covers Done when
1. Ownership 0001 to 0006 Values, stack and heap, moves, Copy, borrows and their rules, slices, String versus &str, shadowing Can predict a move or borrow error before the compiler reports it
2. Data and control 0007 to 0013 Structs, enums, Option and Result, pattern matching and exhaustiveness, iterators, closures, the collections worth knowing Models with enums, and handles absence without reaching for unwrap
3. Errors and API shape 0014 to 0020 Propagation with ?, custom error types, From conversions, panic versus error, modules and visibility, writing documentation that compiles Writes a library whose failures a caller can actually handle
4. Traits, generics and lifetimes 0021 to 0028 Trait bounds, associated types, generics versus dynamic dispatch, the orphan rule, lifetime annotations and elision, why a lifetime is not a duration Reads a lifetime error as information rather than as an obstacle
5. Sharing and threads 0029 to 0036 Box, Rc and Arc, RefCell and Mutex, Send and Sync, threads, channels, deadlock and poisoning Chooses a sharing strategy from the data rather than from habit
6. Async 0037 to 0045 Futures and executors, tasks and cancellation, why a blocking call in async is a bug, locks across await, what pinning is for Writes async code that does not stall, and can explain where it would
7. Unsafe and performance 0046 to 0054 What unsafe actually promises, undefined behaviour, encapsulating an invariant, checking with Miri, benchmarking, allocation and copying costs Can defend an unsafe boundary, and proves a performance claim with a measurement
8. Judgment 0055 to 0063 Publishing, semantic versioning of a public API, the API guidelines, review, reading the standard library and the RFCs for answers Trusted to make the call and to explain it to someone else; stage 9 completes the release procedure's own first step, the test suite passes, with the suite this arc had not yet taught how to write
9. Testing 0064 to 0067 #[test] and cargo test, unit tests versus integration tests, organising tests for a library, property-based and snapshot testing Writes and organises a test suite the way the standard library itself does, not only doctests and benchmarks

Lessons

Work through these in order.

# Lesson Teaches
0001 Ownership and Drop Every value has exactly one owner, and the compiler frees it when that owner goes out of scope
0002 Moves and Copy Assignment moves ownership unless the type is Copy, which is why the old name stops working
0003 Borrowing Many shared borrows or one mutable borrow, never both, and a borrow ends at its last use
0004 Slices, String and str A slice is a borrowed view with a length, and taking &str in an API costs callers nothing
0005 Bindings and Mutability Immutable by default, mut is per binding, and shadowing is a new binding rather than a change
0006 Reading a Borrow Error Five error codes cover most of stage 1, and each one has an honest fix and a workaround
0007 Structs and Their Methods A struct owns its fields, so what you put in one decides who has to keep it alive
0008 Enums That Carry Data An enum says a value is exactly one of these shapes, which is the modelling tool the rest of the stage rests on
0009 Option, and Handling Absence Absence is a value of a different type, so the compiler makes you say what happens when it arrives
0010 Result, and Failure as a Value A failure is a return value rather than an event, so the signature says what can go wrong before you read the body
0011 Pattern Matching A match must cover every case, and the pattern decides whether you borrowed the value or moved it
0012 Iterators and Closures An iterator does nothing until something consumes it, and a closure captures exactly what it uses
0013 The Collections Worth Knowing Vec and HashMap cover most of it, and the entry API is the one thing worth learning properly
0014 Propagating Errors The question mark converts as it returns, so the error type in your signature decides what it will accept
0015 Designing an Error Type A caller can only handle what your error lets them distinguish, so the shape of it is an API decision
0016 Conversions and Boundaries A From implementation is where one layer's failure becomes another's, and a derive writes the ones you already understand
0017 Panic or Error A panic says the program is broken and an error says the input was, and a library rarely gets to decide the first
0018 Modules and Visibility What you make public is what you have promised to keep, so the module tree is an API decision before it is an organisation one
0019 Documentation That Compiles An example in a doc comment is a test, so the documentation that rots is the documentation nobody ran
0020 A Library a Caller Can Handle Split the crate, name the failures, and decide what the binary does that the library must not
0021 Traits as Shared Behaviour How a trait names behaviour several types can provide, and why the trait has to be in scope before you can use it
0022 Trait Bounds and Generic Functions What a bound promises the body and demands of the caller, and what the compiler does with it
0023 Associated Types Why Iterator names its Item as an associated type rather than a parameter, and what that decides for anyone implementing it
0024 Generics or dyn Trait The two dispatch strategies, what each one costs, and the question that actually decides between them
0025 Implementing Traits You Do Not Own Coherence, the newtype pattern, and why a blanket implementation is a commitment rather than a convenience
0026 Lifetimes Are Not Durations What a lifetime parameter says about the relation between borrows, and what elision had been doing for you all along
0027 Types That Borrow Putting a lifetime parameter on a struct, and deciding whether the type should own its data instead
0028 Reading a Lifetime Error The shapes a lifetime error takes, each with the honest fix and the workaround it tempts you into
0029 Threads, Joining and Panics What spawning actually demands of the data you hand it, and what happens to a thread that panics alone
0030 Scoped Threads Why a thread that cannot outlive its scope may borrow, and why this is the first thing to reach for rather than the last
0031 Shared Ownership with Rc and Arc When a value needs several owners rather than one, what the count costs, and how a cycle leaks
0032 Interior Mutability Moving the borrow rule from compile time to run time, what that buys, and what it costs when you get it wrong
0033 Mutex, RwLock and Poisoning What a lock actually protects, why the guard's scope is the design decision, and what a panic while holding one leaves behind
0034 Send and Sync The two traits nobody writes that decide what may cross a thread boundary, and how to read the errors they cause
0035 Channels Sending values between threads instead of sharing them, and what the ends of a channel do when the other end goes away
0036 Choosing a Sharing Strategy The failures that follow from choosing by habit, and the questions that pick the strategy from the data instead
0037 What a Future Is A state machine with one method, driven by whoever polls it, and what that means for code that looks sequential
0038 Wakers, Executors and Runtimes How a future says it is ready to be polled again, and what a runtime does besides poll it
0039 Tasks and the Send Bound What spawning an async task demands of the future you hand it, and why an async trait method will not satisfy it
0040 Blocking Is a Bug Why one blocking call in an async task stalls work that has nothing to do with it, and what to do with the calls you cannot avoid
0041 Shared State Across an Await Why a lock guard held across an await point is refused, and how to choose between the two kinds of mutex
0042 Cancellation Stopping a future means dropping it, which is why nothing gets told and why the work simply stops where it was
0043 Cancellation Safety Whether a future can be dropped mid-flight without losing what it had already taken, and where that question is answered
0044 Pinning Why a future's memory must stop moving once it is polled, and what Pin does about it
0045 Reading an Async Failure The five ways async code fails without an error message, and how to tell which one you are looking at
0046 What Unsafe Promises The five things an unsafe block lets you do, and the obligation you take on by writing one
0047 Raw Pointers What a raw pointer is not, what it costs you in guarantees, and how to make one that is actually valid
0048 Undefined Behaviour What the compiler is allowed to assume, why a program that works can still be wrong, and the list worth knowing by heart
0049 Checking With Miri The tool that turns an invisible soundness bug into a report, what it catches, and what it cannot
0050 Encapsulating an Invariant Wrapping unsafe code in a safe interface, and what has to be true for that interface to deserve the name
0051 Memory Orderings What an ordering actually constrains, why you cannot test one by running it, and how to check the one you chose
0052 Measuring Before Optimising How to get a number you can defend, what the compiler does to a benchmark that measures nothing, and what a measurement cannot tell you
0053 Allocation and Copying Costs Where a Rust program actually spends its time, and which of the obvious fixes pay for themselves
0054 Defending an Unsafe Boundary The argument you have to be able to make before an unsafe block ships, and the measurement that has to come with it
0055 Variance Which lifetimes may stand in for which, why your own type decides, and what that commits you to
0056 Higher-Ranked Bounds Writing the quantifier the compiler has been printing at you, and the API shapes that need it
0057 What Breaks a Caller The changes that need a major version, the ones that only look safe, and the tool that tells you which is which
0058 Designing for Change The decisions that leave you room to move later, and the ones that quietly promise more than you meant
0059 Features and the Minimum Version Why a feature must only add, what unification does to your assumptions, and what declaring a minimum version actually promises
0060 Workspaces and Release Getting a crate to the point where publishing is one command, and what that command actually does
0061 The API Guidelines The checklist the ecosystem already agreed on, which items matter most, and how to use it without cargo-culting it
0062 Reading the Source and the RFCs Answering a question the documentation does not, from the standard library, the tracking issues and the RFCs
0063 Reviewing Rust What to look for in somebody else's Rust, in what order, and which comments are worth making
0064 The Test Attribute and cargo test A doctest lives in a doc comment and runs an example; a #[test] function lives in the crate's own source and is the ordinary shape almost all Rust testing actually takes, and cargo test runs both under the same command without them being the same mechanism
0065 Unit Tests Versus Integration Tests Where a #[test] function lives decides what it can see, the same file as the code under #[cfg(test)] with access to every private item, or a separate tests/ directory that only ever sees what you actually made public
0066 Organising Tests for a Library An integration suite's shape is forced by whatever you already made pub, shared setup goes in tests/common/mod.rs specifically, and a binary with no lib.rs cannot be integration-tested at all
0067 Property-Based and Snapshot Testing This is stage 9's capstone, proptest generates hundreds of inputs and shrinks a failure to the smallest one that still breaks the property, while a snapshot test compares one chosen input's output against a baseline a human already reviewed

Reference

  • Glossary: canonical terms for this topic
  • Resources: trusted sources, each annotated with what it covers
  • Ownership and borrowing: the rules, the Copy list, the error codes, and the honest fix for each
  • The project: the crate the reps build across the arc, and what state it should be in at the end of each stage
  • Data and control: the stage 2 sheet, with the enum and Option and Result decisions, the pattern rules, and the collections
  • Errors and API shape: the stage 3 sheet, with the error-type decisions, the conversion rules, and what a public API commits to
  • Traits and lifetimes: the stage 4 sheet, with the dispatch decision, the coherence rules, and the lifetime error table
  • Sharing and threads: the stage 5 sheet, with the strategy procedure, what each sharing type costs, and the failure table
  • Async: the stage 6 sheet, with the poll model, the bounds, the failure table, and where cancel safety is documented
  • Unsafe and performance: the stage 7 sheet, with the undefined-behaviour table, what each tool catches, and the measurement discipline
  • Judgment: the stage 8 sheet, with the variance rules, the breaking-change table, the release checklist, and the order of authority

How this works

Each lesson is short and self-contained. Answer keys are collapsed: recall first, then open them. The real-world reps matter more than the reading, and spacing them out is the point. Anything still unclear at the end of a lesson is worth chasing to its primary source before moving on.

Table of contents