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
awaitpoint does. - Justify every
unsafeblock 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_stdas 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
unsafeboundary 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.