The Rust Ownership Model
Rust ownership, borrowing, lifetimes, and smart pointers (Box, Rc, Arc, Cell, RefCell) end-to-end. Read any borrow-checker error. Pick the right ownership tool. Course 2 of 12 in the Programming with Rust skill path.
View badge details
About This Course
Internalize the Rust ownership model end-to-end. You go from a one-sentence pitch to fluency with moves, borrows, lifetimes, slices, and the common smart pointers (Box, Rc, Arc, Cell, RefCell, Mutex at the preview level). The borrow checker stops being a mystery and starts being a teacher. By the end you can read any non-unsafe idiomatic Rust and reason about who owns what, who borrows what, and for how long.By the end of this course you will be able to pick the right ownership tool for a situation (move, clone, borrow, Box, Rc, RefCell), read and fix any Beginner-to-Intermediate borrow-checker error (E0382 / E0384 / E0502 / E0505 / E0507 / E0596 / E0597 / E0499), annotate functions with explicit lifetimes when elision rules do not apply, and design small data structures (tree, graph, cache) with the correct ownership shape.Prerequisites: Course 1 (Programming Foundations with Rust) OR confident fluency in another compiled language plus familiarity with references/pointers. Follows the current Rust 2024 edition on stable Rust 1.99.
Course Curriculum
17 Lessons
Why ownership exists — a deeper look
You have the Course 1 vocabulary — every value has an owner, moves transfer ownership, & borrows without taking it. This lesson is the Intermediate deep-dive on why Rust is built this way at all. You compare the three dominant memory models in production (manual, garbage collection, pervasive reference counting), name the specific failure mode each produces at runtime, see the four memory-safety guarantees Rust's ownership and borrowing rules deliver (no use-after-free, no double-free, no data races in safe code, no null-pointer deref), and finish by reading a short Rust program line-by-line and naming the specific bug each line prevents.
By the end of this lesson you will be able to compare the three memory models and name the failure mode of each, explain how Rust's rules deliver each of the four safety guarantees, evaluate when Rust is the right tool for a given project profile versus when a GC-based language fits better, and trace a short Rust program to say what bug each line prevents.
Trace an ownership story - Lab Exercises
Walk a 20-line Rust program through five commented SCENES — Copy value creation, a non-Copy String move, a shared borrow, an explicit clone, and the end-of-scope drop. You annotate each scene with the ownership operation it performs, intentionally break SCENE B by using a moved value, read the compiler's E0382 message carefully, then fix with .clone() and see how two heap allocations coexist. By the end of this short practice you can trace any small Rust program in terms of who owns what, who borrows what, and when destructors run.
Values, moves, and Copy — in depth
Go from Course 1's gentle "a moves to b" story to full fluency with Rust's move semantics and the Copy trait. You will see what the compiler actually does on let b = a for each type category, learn which standard-library types are Copy and why, and build the decision rule for picking between Copy, Clone, and borrowing when you design a function parameter. By the end of this lesson you will be able to predict for any let b = a whether the original binding is still valid, name which fields make a struct Copy or not, and pick between pass-by-value, pass-by-reference, and explicit .clone() for any function signature without guessing.
Move or clone? Practice choosing - Lab Exercises
Train the judgment behind move / clone / Copy / borrow choices. The starter ships six small Rust programs that each fail to compile because of a wrong ownership-tool pick. For each one you read the compiler error, decide between .clone(), &, &mut, or a signature change to take a slice, then apply the smallest fix. By the end you can look at a non-compiling snippet and name which ownership tool it needs — the practical skill behind Lesson 2's Clone-vs-Copy-vs-Drop story.
By the end of this lab you will be able to spot an E0382 "borrow of moved value" error family at a glance, pick between .clone() and &/&mut as the smallest fix, and prefer idiomatic function signatures (slices over Vec, by-value for Copy primitives) in your own code.
References and borrowing — the full picture
Go beyond Course 1's operational "&self vs &mut self vs self" story and internalize how references really work in Rust. You'll study the shared-borrow / exclusive-borrow rule that the compiler enforces, the data-race prevention guarantee that rule gives you, non-lexical lifetimes (borrows end at last use, not at scope end), reborrowing (how &mut T can be passed into a function and still used after), and borrow splitting across struct fields.
By the end of this lesson you will be able to describe the aliasing rules Rust enforces between &T and &mut T and name the specific class of bugs those rules prevent, trace a non-lexical lifetime through a short program and explain exactly where each borrow ends, and apply reborrowing to pass &mut T into a function and keep working with the parent reference after the call returns.
Shared vs exclusive borrows practice - Lab Exercises
Internalize when the borrow checker lets &T and &mut T coexist and when it does not. You run five small drill binaries that each exercise one pattern — reader fn + writer fn in sequence, split borrows across struct fields, non-lexical lifetimes ending at last use, reborrow for passing &mut *outer to an inner function, and the collect-then-push fix for iterator invalidation. By the end you can read the compiler's E0502 output and know exactly which fix family to reach for.
The borrow checker as a teacher — reading error codes
Read any rustc borrow-checker error output with precision — title line, source span, note blocks, help suggestions — and group the top 8 error codes (E0382, E0384, E0499, E0502, E0505, E0507, E0596, E0597) into three families (move, mutability, lifetime). You walk away able to look at a novel error, extract which safety guarantee is at stake from the shape of the message, and choose a minimal fix. This is the Intermediate skill that turns rustc from an obstacle into the most patient teacher you have.
By the end of this lesson you will be able to identify each region of a rustc error output in a novel message, identify which family a given error belongs to from the code snippet alone, explain the memory-safety guarantee each family prevents, and choose a minimal fix strategy for each family.
Fix the borrow checker 2.0 — 12 harder drills - Lab Exercises
Twelve harder borrow-checker drills, each failing with a specific rustc error code — E0382 / E0384 / E0499 / E0502 / E0505 / E0507 / E0515 / E0596 / E0597. By the end you can read any Beginner-to-Intermediate borrow-checker error and apply the smallest fix the compiler is asking for.
The point of this lab is NOT memorizing error codes — it is building the reflex to read the "value moved here" / "mutable borrow occurs here" / "value does not live long enough" annotations rustc generates and locate the line that needs to change.
Slices — &[T], &str, and views into data
Fluency with Rust's slice types — &[T] for sequences and &str for text. A slice is a borrowed window into contiguous data: no ownership, no allocation, just a pointer plus a length that lets a function work with Vec, array, or sub-slice through one signature. You will learn the memory layout a slice compiles to, the handful of methods you reach for daily (.get, .first, .last, iteration, range syntax), and the UTF-8 byte-boundary rules that keep &str slicing safe. You finish by rewriting function signatures from the restrictive &Vec<T> to the idiomatic &[T] and from &String to &str.
By the end of this lesson you will be able to describe a slice as a borrowed view and relate &[T] to its owning Vec<T>, apply &str slicing without triggering char-boundary panics, and evaluate function parameter choices between slice-of-T and reference-to-container.
Slice-ify — refactor to &[T] and &str - Lab Exercises
Refactor three functions from owned-container references (&Vec<T>, &String) to the more general slice idioms (&[T], &str), fix a UTF-8 char-boundary panic in a string helper, and implement a non-allocating tokenizer that returns slices borrowed from its input. By the end of this hands-on lab you can read any function signature in the Rust ecosystem and tell which parameter shape (owned, slice, iterator) is idiomatic for the job.
Lifetimes made concrete
Lifetimes are the single hardest Intermediate concept in Rust — and the one that most students decide they'll "just add &'a everywhere and hope." This lesson replaces that habit with a precise model. You'll see what 'a actually represents (a region of code, not a duration), apply the three elision rules the compiler uses to infer most lifetimes automatically, pick the right annotation when elision fails, and attach lifetimes to structs that hold references.
By the end of this lesson you will be able to read any Rust fn signature and name the lifetime relationships at play, apply the three elision rules in your head to any signature, identify when 'static is required (and when it's merely convenient), and declare a struct that holds references with the correct lifetime parameters.
Lifetime annotations in practice - Lab Exercises
Fix three broken items in a small Rust program by applying the lifetime annotations and elision rules you just learned. You start with a fn longest, a fn first_word, a struct Excerpt, and a fn use_excerpt. Two of the three functions and the struct refuse to compile — the Rust compiler is telling you exactly where it needs a 'a to tie borrowed references together. By the end of the lab you will have added lifetime annotations where elision does not cover them, documented why the fourth signature does not need any, and produced a working program that prints a longest string, a first word, and an excerpt back from a borrowed source.
Smart pointers — Box, Rc, Arc
Understand Rust's three "ownership on the heap" tools — Box<T> for single-owner heap allocation, Rc<T> for shared single-threaded immutable ownership, and Arc<T> for shared ownership across threads. You learn when to reach for each one, when a plain borrow is enough, and why Arc pays an atomic-op cost that Rc avoids.
By the end of this lesson you will be able to pick the right smart pointer for a given data-sharing pattern, explain why recursive enum types need Box<T>, trace Rc's clone-and-drop ref-count mechanics, and articulate the Send + Sync reason the compiler forces Arc<T> whenever you cross a thread boundary.
Trees with Box, graphs with Rc - Lab Exercises
Build three small data structures, each picked to force a different smart-pointer choice. First a binary tree where every node owns its two children — the ideal case for Box because ownership is exactly one. Then a shared-file structure where directories share the same File without duplicating its contents — the case Rc is designed for. Finally, convert that File to Arc and send it across a thread boundary, observing why Rc wouldn't compile there.
By the end of this lab you can pick Box or Rc or Arc for a given data-structure shape and justify the choice, construct recursive owned types without fighting the compiler, and describe the single-line difference between Rc and Arc at the Send/Sync level.
Interior mutability — Cell, RefCell, and when runtime enforcement is right
Learn when the compile-time borrow checker is too conservative for a correct program, and how Cell, RefCell, and the Rc<RefCell<T>> composition pattern move the same borrow rules from compile time to runtime. You pick the right tool for shared counters, cache-and-memoize patterns, graph structures, and the small handful of situations where Rust's single-mutable-owner rule produces unnecessary friction.
By the end of this lesson you will be able to explain why interior mutability exists in Rust, choose between Cell and RefCell based on whether the inner type is Copy, recognise and avoid the common BorrowMutError runtime panic, apply the Rc<RefCell<T>> pattern for shared mutable state in single-threaded code, and preview where Mutex / RwLock take over for thread-safe equivalents in Course 5.
Prerequisite: Course 2 Lessons 1-7. Follows the current Rust 2024 edition on stable Rust 1.99.
Build a shared memoizing cache with Rc<RefCell> - Lab Exercises
Share a memoizing Fibonacci cache between two closures using Rc<RefCell<HashMap<u64, u64>>>. You rewrite a slow recursive baseline into a memoized version, exercise Rc::clone to let multiple callers hold references to the same RefCell, trigger a BorrowMutError panic and fix it with a scope, then preview Arc<Mutex<T>> for the thread-safe equivalent. By the end of the lab you can pick between &mut, Rc<RefCell>, and Arc<Mutex> for a shared-mutable-state situation.
Capstone — Convert a linked list from Box to Rc<RefCell> - Lab Exercises
Course 2 synthesis capstone. You take a working Box<Node>-based singly-linked list and refactor it to Rc<RefCell<Node>> so two lists can share the same tail subtree — and mutate the shared tail through either list. By the end you have internalized every ownership tool Course 2 taught: moves vs clones vs shares, interior mutability, strong_count semantics, when Rc<RefCell<T>> is the right pattern and when it is a smell. Final output: a small graph-shaped linked list with provable shared ownership + mutable access across both lists.