Checkpoint: Systems Rust
Reviewed & published by Brayan K
This checkpoint consolidates the systems-programming half of the course — extra collections, macros, Drop and RAII, newtypes, Cow, Box<dyn Error>, and unsafe — into one build challenge and a self-test quiz.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
You'll recap each topic, then write a small task runner that combines a validated newtype, a custom macro, and Result-based error handling, with a full worked solution to check yourself against.
What This Checkpoint Reviews
- Specialized collections (VecDeque, BTreeMap, BinaryHeap)
- Declarative macros with macro_rules!
- The Drop trait and RAII cleanup
- The newtype pattern and type aliases
- Cow clone-on-write and Box<dyn Error>
- unsafe Rust and its five superpowers
1️⃣ Section Recap
Before the challenge, here is the systems-Rust toolkit in one place. Each row links a concept to the one-line idea you should remember.
| Topic | Key idea |
|---|---|
| Collections | VecDeque for both ends, BTreeMap for sorted/range, BinaryHeap for priority |
| Macros | macro_rules! matches tokens and expands code at compile time |
| Drop / RAII | Cleanup runs at scope end, in reverse declaration order |
| Newtype | Wrap a type for safety and to beat the orphan rule |
| Cow | Borrow for free, allocate only when you modify |
| Box<dyn Error> | One error type for all failures; ? converts and propagates |
| unsafe | Five superpowers; keep it small, sound, and wrapped |
2️⃣ Build Challenge: A Validated Task Runner
Now put three of those tools together. You'll build a Priority newtype that only accepts values 1–5, a task_queue! macro that constructs a VecDeque of tasks, and a process function that validates each task and propagates failures through Box<dyn Error>. Try it yourself first.
// 🎯 BUILD CHALLENGE — combine a newtype, a macro, and Result errors.
//
// Build a tiny task runner that ties together everything in this section.
//
// 1) NEWTYPE: a validated Priority(u8) that is only valid for 1..=5.
// - Give it Priority::new(value: u8) -> Result<Priority, Box<dyn Error>>.
// - Make an error type BadPriority that implements Debug + Display + Error.
//
// 2) MACRO: task_queue! { "name" => prio, ... } that builds a
// VecDeque<(&str, u8)> by push_back-ing each pair.
//
// 3) ERROR HANDLING: fn process(queue) -> Result<(), Box<dyn Error>>
// pops tasks from the FRONT, validates each priority with Priority::new(...)?,
// and prints "running '<name>' at priority <p>".
//
// In main, run a valid queue (all priorities 1..=5) and a bad one
// (a priority like 9) and print the outcome of each with a match.
//
// ✅ Expected output:
// running 'backup' at priority 5
// running 'email' at priority 2
// running 'report' at priority 3
// all good tasks done
// error: priority 9 is out of range (must be 1..=5)
use std::collections::VecDeque;
use std::error::Error;
use std::fmt;
fn main() {
// your code here — see the worked solution below if you get stuck
}Here is a complete, compiling reference implementation. Compare it against your own attempt — there is more than one valid way to structure it.
3️⃣ 📝 Checkpoint Quiz
Answer each question in your head first, then click to reveal. If any feel uncertain, revisit that lesson before the capstone.
VecDeque. It is a ring buffer, so push_front, pop_front, push_back, and pop_back are all O(1). A plain Vec is O(n) to remove from the front.
Wrap each value in std::cmp::Reverse before pushing. Reverse flips the ordering, so the smallest underlying value pops first.
They are dropped at the end of the scope, in the reverse of their declaration order — the last value created is the first destroyed, like unwinding a stack.
A newtype is a genuinely distinct type, so the compiler stops you mixing Meters with Feet and lets you implement foreign traits on it. A type alias is just a nickname for the same type and adds no safety.
It unwraps an Ok value, or on an Err returns early — automatically converting the concrete error into Box<dyn Error> via the From trait.
No. Normal references, ownership, and the type system are all still enforced. Unsafe only enables five extra operations (such as dereferencing raw pointers); raw pointers are simply not tracked by the borrow checker in the first place.
Pro Tips
- 💡 Build in stages. Get each of the three pieces compiling on its own before wiring them together.
- 💡 Let the types do the checking. Validating in Priority::new means an invalid priority can never exist downstream.
- 💡 Score yourself honestly. Any quiz answer you weren't sure of points straight at the lesson to review.
🎉 Checkpoint Complete!
- ✅ Recapped collections, macros, Drop/RAII, newtypes, Cow, Box<dyn Error>, and unsafe
- ✅ Built a task runner combining a newtype, a macro, and Result error handling
- ✅ Checked your understanding against a full worked solution
- ✅ Self-tested with the checkpoint quiz
Practice quiz
Which standard collection gives O(1) push and pop at BOTH the front and the back?
- Vec
- BTreeMap
- VecDeque
- BinaryHeap
Answer: VecDeque. VecDeque is a ring buffer, so push_front, pop_front, push_back, and pop_back are all O(1). A plain Vec is O(n) to remove from the front.
How do you turn a BinaryHeap (a max-heap) into a min-heap?
- Wrap each value in std::cmp::Reverse before pushing
- Call .reverse() on the heap
- Use BinaryHeap::min() instead of new()
- It is impossible without a custom struct
Answer: Wrap each value in std::cmp::Reverse before pushing. Wrapping values in std::cmp::Reverse flips the ordering, so the smallest underlying value pops first, giving min-heap behavior.
In what order are values dropped within a scope?
- In declaration order (first declared, first dropped)
- In a random order
- All at once with no defined order
- In reverse declaration order (last declared, first dropped)
Answer: In reverse declaration order (last declared, first dropped). Values are dropped at the end of the scope in the reverse of their declaration order, like unwinding a stack.
Why use a newtype like 'struct Meters(f64)' instead of a 'type' alias?
- A newtype is faster at runtime
- A newtype is a genuinely distinct type and lets you implement foreign traits on it
- A type alias cannot hold an f64
- There is no real difference
Answer: A newtype is a genuinely distinct type and lets you implement foreign traits on it. A newtype is a distinct type, so the compiler stops you mixing Meters with Feet and lets you implement foreign traits. A type alias is just a nickname and adds no safety.
For a function returning Result<T, Box<dyn Error>>, what does the ? operator do on an Err?
- It returns early, converting the concrete error into Box<dyn Error> via From
- It panics immediately
- It ignores the error and continues
- It loops until the call succeeds
Answer: It returns early, converting the concrete error into Box<dyn Error> via From. ? unwraps an Ok value, or on an Err returns early, automatically converting the concrete error into Box<dyn Error> through the From trait.
Does an 'unsafe' block turn off the borrow checker?
- Yes, it disables all of Rust's safety checks
- Yes, but only for references
- No; it only enables five extra operations such as dereferencing raw pointers
- No, it has no effect on what code can do
Answer: No; it only enables five extra operations such as dereferencing raw pointers. unsafe does not disable the borrow checker. Normal references, ownership, and the type system are still enforced; unsafe only unlocks five extra operations.
What does macro_rules! operate on, and when does it expand?
- Runtime values, expanded at runtime
- Tokens, expanded at compile time
- Bytes, expanded at link time
- Strings, expanded by the OS
Answer: Tokens, expanded at compile time. macro_rules! is a declarative macro that matches tokens and expands into code at compile time.
What is Cow (clone-on-write) primarily used for?
- Forcing every value to be cloned eagerly
- Sharing data across threads safely
- Replacing Box<dyn Error>
- Borrowing data for free and allocating only when you actually need to modify it
Answer: Borrowing data for free and allocating only when you actually need to modify it. Cow lets you borrow data for free and only allocates/clones when modification is actually needed, avoiding unnecessary allocations.
Which trait do you implement so that custom cleanup runs when a value goes out of scope?
- Clone
- Drop
- Default
- Copy
Answer: Drop. The Drop trait lets you define cleanup logic (drop) that runs automatically at the end of a value's scope — the basis of RAII in Rust.
In the task runner, what does Priority::new return so its constructor can validate the range 1..=5?
- Priority directly
- Option<Priority>
- Result<Priority, Box<dyn Error>>
- u8
Answer: Result<Priority, Box<dyn Error>>. Priority::new returns Result<Priority, Box<dyn Error>>, returning Ok for a valid value and an Err (BadPriority) when the value is out of the 1..=5 range.
Continue this course
- Previous: unsafe Rust
- Next: Capstone: A CLI Tool — Bring it all together by building a real command-line application
- Quick reference: Rust cheat sheet
Frequently asked questions
What does this checkpoint cover?
This checkpoint consolidates the systems-Rust section: the specialized standard collections (VecDeque, BTreeMap, BinaryHeap, and the set types), declarative macros with macro_rules!, the Drop trait and RAII for deterministic cleanup, the newtype pattern and type aliases, Cow for clone-on-write, error handling with Box<dyn Error>, and unsafe Rust. The build challenge weaves several of these together — a validated newtype, a custom macro, and Result-based error handling — so you can see how they cooperate in a single small program rather than in isolation.
How should I use the build challenge?
Try to write the whole program yourself first, from the starter comments, before opening the worked solution. Build it in three stages matching the numbered steps: get the Priority newtype and its BadPriority error compiling, then add the task_queue! macro, then write process and main and wire them together with the ? operator. Compile after each stage. Only reveal the Show solution panel once you have made a genuine attempt — comparing your version against a reference is far more instructive than reading the answer cold.
Why combine a newtype, a macro, and error handling in one program?
Because that is how these features show up in real code. A newtype gives you a validated, self-documenting type; a macro removes boilerplate when constructing data; and Box<dyn Error> plus the ? operator lets failures from any step propagate cleanly to the top. Seeing them together demonstrates Rust's central theme: push correctness into the type system so that invalid states are hard to represent, while keeping the surface syntax ergonomic. Each feature is useful alone, but their combination is where idiomatic Rust really takes shape.
What comes after this checkpoint?
Next is the Capstone Project, where you bring together everything from the entire course — ownership and borrowing, traits and generics, collections, error handling, and the systems features from this section — to build a complete, larger program end to end. The checkpoint is your readiness check: if the build challenge and the quiz below feel comfortable, you are well prepared to tackle the capstone. If any topic feels shaky, revisit that lesson before moving on.