Rust Roadmap

Rust is a systems programming language that gives you control over memory and speed without a garbage collector, and checks at compile time that you use that control safely. It is used for command line tools, network services, embedded devices, game engines, WebAssembly modules and parts of operating systems and browsers.

The language has a reputation for being hard, and the reputation is earned in one place: ownership and borrowing. This roadmap puts those ideas early, explains them with small examples, and builds everything else on them. Each stage lists topics in learning order with a short exercise, followed by projects, interview questions with model answers, common mistakes and a readiness checklist.

By the end you should be able to write idiomatic Rust, model data with enums and traits, handle errors properly, write safe concurrent and async code, and explain why the compiler accepts or rejects a program.

Level
Beginner to advanced
Time
14-20 weeks at about 8 hours a week
Scope
57 topics in 9 stages
Before you start
Programming experience in any language, including functions and data structures; Comfort with the command line; Helpful: basic understanding of memory, the stack and the heap, as in Computer Science Roadmap

What you will be able to do

  • Explain ownership, borrowing and lifetimes, and fix borrow checker errors
  • Model data with structs, enums, Option and Result, and process it with pattern matching
  • Write generic code with traits, trait bounds and trait objects
  • Handle errors with the question mark operator and custom error types
  • Write thread-safe code with channels, Arc and Mutex, and async code with Tokio
  • Test, document, benchmark and publish crates with Cargo
  • Build a command line tool and a web service using the main ecosystem crates

The roadmap at a glance

  1. Stage 1Getting Started with RustBeginner · 1 week · 5 topics
  2. Stage 2Language BasicsBeginner · 2 weeks · 6 topics
  3. Stage 3Ownership and BorrowingBeginner · 2-3 weeks · 5 topics
  4. Stage 4Types, Pattern Matching and OrganisationIntermediate · 2 weeks · 5 topics
  5. Stage 5Collections and Error HandlingIntermediate · 2 weeks · 5 topics
  6. Stage 6Traits, Generics and Smart PointersIntermediate · 2-3 weeks · 6 topics
  7. Stage 7Concurrency and AsyncAdvanced · 3 weeks · 5 topics
  8. Stage 8Macros, Testing and ToolingAdvanced · 2-3 weeks · 10 topics
  9. Stage 9Ecosystem and ApplicationsAdvanced · 4 weeks · 10 topics

Stage 1 Getting Started with Rust

Beginner1 week5 topics

Goal. Install the toolchain, create a Cargo project, and run and test a first program.

Introduction: What is Rust? Why use Rust? Core

Rust is a compiled systems language that guarantees memory safety without a garbage collector. The compiler checks ownership and borrowing rules, so whole classes of bugs (use after free, double free, data races) are rejected before the program runs.

People choose it for command line tools, network services, embedded software, game engines, browsers' components and WebAssembly, where they need C-like speed and predictable resource use without the usual memory bugs. The price is a steeper learning curve: the compiler is strict, and you spend your first weeks learning its rules. Most learners find that errors become clear after the ownership topics in stage three. See Modern C++ Roadmap and Go Roadmap for neighbouring languages.

Practice. Write down three bugs common in C programs that the Rust compiler prevents.

Installing Rust and Cargo Core

Install through rustup, the official toolchain manager. It installs the compiler (rustc), the package manager and build tool (cargo) and can switch between the stable, beta and nightly channels. Use stable unless a tool tells you otherwise.

Useful commands: rustup update, rustup component add clippy rustfmt, rustup target add for cross-compilation and rustc --version to confirm the install. A new release arrives about every six weeks, and the language keeps backwards compatibility within an edition (2015, 2018, 2021, 2024), which you choose in Cargo.toml. Older code compiles on newer compilers.

Practice. Install rustup, add clippy and rustfmt, and print the versions of rustc and cargo.

IDEs and Rust Toolchains, and the Rust REPL Recommended

The language server rust-analyzer gives completion, inline type hints, error messages and refactoring in VS Code, RustRover, Neovim, Helix and others. Turning on inlay hints for types helps a lot while learning.

Two helpers are worth knowing. The Rust Playground, a web page that compiles and runs snippets and shares them by link, serves as the Rust REPL, since the language has no built-in interactive shell. Tools such as evcxr provide a true REPL if you want one locally. For toolchains, you can pin a version per project with a rust-toolchain.toml file so that everyone builds with the same compiler.

Practice. Open a project in an editor with rust-analyzer, and use type hints to find the inferred type of three variables.

Hello World and Your First Cargo Project Core

cargo new hello creates a folder with Cargo.toml and src/main.rs:

fn main() {
    println!("Hello, Rust");
}

println! ends with an exclamation mark because it is a macro, not a function. Build and run with cargo run; cargo check type-checks without producing a binary and is much faster; cargo build --release builds an optimised binary into target/release. Debug builds are slow on purpose, so always benchmark release builds.

Add target/ to .gitignore (Cargo does this for new projects) and commit Cargo.lock for applications.

Practice. Create a project, print a greeting from command line arguments and compare debug and release build times.

Dependency Management with Cargo.toml Core

Cargo.toml describes the package: name, version, edition and dependencies. Add one with cargo add serde, which writes a line in the [dependencies] table. Versions use semantic versioning ranges, so 1.2 means at least 1.2.0 and below 2.0.0. Cargo.lock records exact versions and makes builds repeatable.

Features switch optional parts of a crate on or off: serde = { version = "1", features = ["derive"] }. Development-only crates go in [dev-dependencies]. Workspaces group several crates in one repository with a shared lock file and target folder. Use cargo tree to see the dependency graph and cargo update to move within allowed ranges. Check unfamiliar crates for maintenance and licence before adding them.

Practice. Add serde with the derive feature to a project and inspect the result with cargo tree.

Stage 2 Language Basics

Beginner2 weeks6 topics

Goal. Write programs using Rust values, text, functions and control flow.

Variables, Constants and Data Types Core

Variables are immutable by default: let x = 5; cannot be reassigned, while let mut x = 5; can. Shadowing with a new let x = ... creates a fresh variable, which can even change type. Constants use const MAX: u32 = 100;, need a type annotation and are inlined at compile time; static items have a fixed address for the whole program.

Rust is statically typed with strong inference, so you write types mainly on function signatures. Unused variables get a warning; prefix them with an underscore to silence it. Integer overflow panics in debug builds and wraps in release builds unless you ask for explicit behaviour with checked_add, wrapping_add or saturating_add.

Practice. Show shadowing changing a variable from a string to its length, then trigger and fix an overflow with checked_add.

Integers, Floats, Boolean and Character Core

Integer types are signed i8 to i128 and unsigned u8 to u128, plus pointer-sized isize and usize. usize is used for indexing and lengths. The default integer is i32. Conversions are explicit with as (which can truncate) or safer try_from and into.

Floats are f32 and f64 (the default); NaN is not equal to itself, so floats implement only PartialEq and PartialOrd. A boolean, the bool type, is true or false.

A char is four bytes and holds one Unicode scalar value, written in single quotes, unlike a byte b'a'. Because of this, a char is not the same as one byte of a string. Methods like is_alphabetic, to_ascii_uppercase and to_digit cover common checks.

Practice. Convert a u64 to u8 using as and using try_from, and compare the outcomes for a value of 300.

Tuples and Arrays Core

A tuple groups values of different types with a fixed length: let t = (1, "a", 2.5);, accessed as t.0 or destructured with let (a, b, c) = t;. The empty tuple () is the unit type, returned by functions that return nothing.

An array has a fixed length known at compile time and one element type: let a = [1, 2, 3]; or [0; 100] to repeat a value. Its type includes the length, [i32; 3], and it lives on the stack. Indexing is bounds-checked and panics when out of range. For growable sequences use Vec<T>, and use slices (&[T]) to refer to any contiguous run of elements, whether it came from an array or a vector.

Practice. Write a function that takes a slice of integers and returns a tuple with the minimum and maximum.

Strings: String and &str Core

Rust has two main string types. &str is a borrowed view of UTF-8 text, such as a literal. String is an owned, growable buffer on the heap. Functions that only read text should accept &str; a &String converts to it automatically.

let mut s = String::from("hello");
s.push_str(", world");
let part: &str = &s[0..5];

Strings are always valid UTF-8, so indexing by number (s[0]) is not allowed; slicing needs byte positions on character boundaries and panics otherwise. Iterate with s.chars() for characters or s.bytes() for bytes. Use format! to build strings, and to_string() or to_owned() to convert from &str.

Practice. Count the characters and bytes in a string with an accented letter, and reverse its words.

Functions and Method Syntax Core

Functions are declared with fn, with typed parameters and an arrow for the return type. The last expression in a block, without a semicolon, is its value, so fn add(a: i32, b: i32) -> i32 { a + b } needs no return. Adding a stray semicolon turns that expression into a statement and gives a type error that confuses beginners.

Methods are functions defined in an impl block with self, &self or &mut self as first parameter, and they are called with dot syntax. Functions without self are associated functions, called as Type::new(). Closures are unnamed functions written as |x| x + 1. They may use variables defined around them, and come in Fn, FnMut and FnOnce flavours depending on what they do with those variables.

Practice. Write a function and a closure that both double a number, and pass the closure to a function taking impl Fn.

Control Flow Constructs Core

if is an expression, so let x = if cond { 1 } else { 2 }; works; the condition must be a bool. Loops are loop (infinite, can return a value through break value), while and for. A for loop iterates over anything that implements IntoIterator, such as ranges 0..10 or 1..=10, vectors and iterators. Labels like 'outer: let break and continue target an outer loop.

match is the main branching tool and must be exhaustive, so the compiler tells you when you forget a case. if let and while let handle a single pattern, and let else lets you bail out early when a pattern does not match. Prefer iterators and for over index loops; they are clearer and avoid bounds checks.

Practice. Solve FizzBuzz with match on a tuple, then rewrite a counting loop with iterator methods.

Stage 3 Ownership and Borrowing

Beginner2-3 weeks5 topics

Goal. Explain ownership, borrowing and lifetimes, and read borrow checker messages without panic.

Ownership Rules and Memory Safety Core

Ownership is Rust's way of managing memory without a collector. The rules are short. A value always has an owner. It never has two. When that owner leaves scope, the value is dropped and its memory and resources are released.

Assigning or passing a non-Copy value moves it: the old variable can no longer be used.

let a = String::from("hi");
let b = a;        // a is moved
// println!("{a}"); // compile error
let c = b.clone(); // explicit deep copy

Simple types like integers and bool implement Copy and are duplicated instead. Drop runs deterministically, which is why files and locks close at scope end. This is the same idea as RAII in Modern C++ Roadmap, but checked by the compiler.

Practice. Write code that moves a String into a function, observe the error, then fix it three ways: clone, borrow and return it.

Deep Dive: Stack vs Heap Recommended

The stack holds fixed-size values in a last-in first-out frame per function call; allocation and release is just moving a pointer, so it is very fast. The heap holds data whose size is unknown at compile time or that must outlive the call; allocating takes more work, and you refer to it through a pointer.

A String or Vec is a small stack value (pointer, length, capacity) that points to heap data. Moving it copies only those three words; the heap buffer stays put. Box<T> puts a single value on the heap.

Knowing this explains many rules: why moves are cheap, why Copy types must be plain data, why recursive types need a Box and why returning a reference to a local variable is rejected.

Practice. Draw the stack and heap for a String variable and its clone, and say what is freed when each goes out of scope.

Borrowing, References and Slices Core

A reference lets you use a value without owning it. &T is a shared borrow and &mut T an exclusive one. The borrow rules: at any time you may have many shared references or exactly one mutable reference, never both, and references must not outlive the value. These rules prevent data races and iterator invalidation at compile time.

let mut v = vec![1, 2, 3];
let first = &v[0];
// v.push(4); // error while first is still used
println!("{first}");

Since Rust 2018 the compiler uses non-lexical lifetimes: a borrow ends at its last use, not at the end of the block. A slice such as &v[1..3] or &str is a reference to part of a collection, stored as pointer plus length.

Practice. Write a function that takes a mutable reference to a vector of integers and removes duplicates, and call it while also holding a shared reference to see the error.

Lifetimes and Borrow Checker Core

A lifetime is the span in which a reference stays valid. The borrow checker compares them to make sure no reference outlives its data. Most of the time they are inferred, and you meet them in errors such as "borrowed value does not live long enough".

A function returning a reference must say which input it borrows from, since the compiler cannot guess from the signature alone. When fixing errors, ask first whether you should return an owned value instead; cloning or returning String is often the right answer for beginners. Do not fight the checker by adding annotations at random: restructure so that data has one clear owner and short, local borrows.

Practice. Write a function that returns a reference to a local variable, read the error and fix it by returning an owned value.

Explicit Lifetime Annotations and Elision Rules Core

Lifetime annotations use apostrophes and describe relationships, not durations:

fn pick_first<'a>(left: &'a str, right: &'a str) -> &'a str {
    if left < right { left } else { right }
}

Here the returned reference is valid only as long as the shorter-lived of the two inputs. Structs that store references need them too: struct Parser<'a> { text: &'a str }. The 'static lifetime means data that lives for the whole program, such as string literals; a T: 'static bound means a type owns its data and has no short borrows.

Lifetime elision rules let you omit most annotations: each reference input gets its own lifetime; if there is exactly one input lifetime it is given to all outputs; and if one input is &self or &mut self, its lifetime is given to outputs.

Practice. Write a struct that holds a &str slice from a larger string and a method returning the first word with correct lifetimes.

Stage 4 Types, Pattern Matching and Organisation

Intermediate2 weeks5 topics

Goal. Model data with structs and enums, handle absence and failure, and organise code in modules and crates.

Structs and Impl Blocks Core

A struct groups named fields: struct User { name: String, age: u32 }. Tuple structs (struct Meters(f64);) and unit structs also exist; the tuple form is used for the newtype pattern, which gives a distinct type to a plain value. Create with User { name, age }, and copy remaining fields from another value with ..other.

Behaviour goes in impl blocks. Constructors by convention are an associated function called new. Derive common traits with #[derive(Debug, Clone, PartialEq)]. Fields are private to the module unless marked pub, so you can keep invariants by exposing methods only. Several impl blocks per type are allowed.

Practice. Define a Rectangle with a constructor, an area method and a method that scales it in place.

Enums Core

Rust enums are sum types: each variant can carry different data.

enum Shape {
    Circle(f64),
    Rect { w: f64, h: f64 },
    Empty,
}

A value is exactly one variant, and match forces you to handle them all, so adding a variant makes the compiler list every place that needs updating. Enums model states, messages and commands far better than a struct of optional fields. They can have methods, and #[derive] works on them. C-like enums without data convert to integers with as. Use #[non_exhaustive] on public enums when you may add variants later.

Practice. Model a traffic light or a payment status as an enum and write a method that returns the next state.

Option and Result Enumerations Core

Rust has no null. A value that may be absent is Option<T> (Some(v) or None), and an operation that may fail returns Result<T, E> (Ok(v) or Err(e)). Because they are ordinary enums, the compiler forces you to deal with the missing or failing case.

Handle them with match, if let, or combinators: map, and_then, unwrap_or, unwrap_or_else, ok_or, filter. unwrap() and expect("msg") panic on failure; they are fine in tests and quick scripts, or where a failure would be a programming bug, but not for expected errors. Clippy warns about careless unwraps in production code if you enable the lint.

Practice. Write a function that parses a port number from a string and returns Result, and another that returns the first even number as Option.

Pattern Matching and Destructuring Core

Patterns appear in match, let, function parameters, if let and for. They can destructure tuples, structs and enums, bind parts with names, ignore with _ or .., match ranges (1..=5), combine with | and add conditions with guards:

match msg {
    Shape::Rect { w, h } if w == h => println!("square"),
    Shape::Rect { w, .. } => println!("width {w}"),
    Shape::Circle(r) => println!("circle {r}"),
    Shape::Empty => {}
}

Use @ to bind a value while testing it (n @ 1..=9). Matching on references follows "default binding modes", so you rarely write ref by hand now. Exhaustiveness checking makes refactors safe.

Practice. Write a match over a pair of a boolean and an optional integer that covers every combination without a wildcard.

Modules and Crates, Code Organization and Namespacing Core

A crate is a compilation unit: a binary with main.rs or a library with lib.rs. A package, described by one Cargo.toml, can hold a library and several binaries. Modules organise code inside a crate: mod parser; loads parser.rs or parser/mod.rs. Items are private by default; use pub, pub(crate) for crate-only visibility and pub use to re-export a tidier public path.

Bring names into scope with use std::collections::HashMap; and group with braces. Paths start from crate::, self:: or super::. Namespacing is therefore explicit and tied to the file layout. Keep the public API small and put tests in a tests module or folder. For big projects, split into workspace crates to speed up compilation and enforce boundaries.

Practice. Split a program into a library crate and a binary, exposing three public functions and keeping one private.

Stage 5 Collections and Error Handling

Intermediate2 weeks5 topics

Goal. Pick the right collection and handle errors with Result, the question mark operator and custom error types.

Vector and Collections Overview Core

Vec<T> is the default growable array: vec![1, 2, 3], push, pop, len, indexing with v[i] (panics) or v.get(i) (returns Option). Capacity grows by doubling; use Vec::with_capacity(n) when you know the size. Iterate with for x in &v, &mut v or v (which consumes it).

The standard library collections are Vec, VecDeque, LinkedList, HashMap, BTreeMap, HashSet, BTreeSet and BinaryHeap. Choose by access pattern: Vec unless you have a clear reason. The next topics cover the others. All collections own their elements and free them when dropped.

Practice. Read numbers from standard input into a Vec, then print the sorted list and the median.

HashMap, HashSet and Their Ordered Cousins Core

HashMap<K, V> stores key-value pairs with average constant-time lookup; keys need Eq + Hash. The entry API avoids double lookups:

let mut counts = HashMap::new();
for w in text.split_whitespace() {
    *counts.entry(w).or_insert(0) += 1;
}

HashSet<T> stores unique values and supports union, intersection and difference. The default hasher is SipHash, which resists hash-flooding attacks but is slower than simple hashers; swap it for FxHash or ahash when keys are trusted and speed matters. Iteration order is random.

BTreeMap and BTreeSet keep keys sorted, support range queries and need Ord. Pick them when you need ordered iteration or ranges.

Practice. Count word frequencies with the entry API and print the ten most frequent words.

Queues, Stacks, BinaryHeap and LinkedList Recommended

A stack is just a Vec with push and pop. A queue is a VecDeque, a ring buffer with fast push_back and pop_front. BinaryHeap<T> is a max-heap giving the largest item first, which is how you build a priority queue; wrap items in std::cmp::Reverse for a min-heap, and implement Ord on a struct for custom priority (as Dijkstra's algorithm needs).

LinkedList exists but is rarely the right choice: cache misses make it slower than Vec or VecDeque for almost everything, and linked structures are awkward under ownership rules. Be ready to explain this in interviews, and see Data Structures and Algorithms Roadmap for the algorithms that use these structures.

Practice. Implement a breadth-first search on a small graph using VecDeque and a task scheduler using BinaryHeap.

Error Handling, Propagating Errors and ? Operator Core

Expected failures return Result. The ? operator unwraps Ok or returns the Err from the current function early, converting the error type through the From trait:

fn read_port(path: &str) -> Result<u16, Box<dyn std::error::Error>> {
    let text = std::fs::read_to_string(path)?;
    Ok(text.trim().parse()?)
}

It also works on Option in functions returning Option. main itself can return Result. Panics (panic!, unwrap, index out of range) are for bugs and unrecoverable states; they unwind the stack and end the thread. Libraries should return errors rather than panic. Add context to errors as they pass upward so that messages say what was being attempted.

Practice. Write a function that reads a file, parses each line as a number and returns the sum, using ? throughout.

Custom Error Types and Traits Core

For a library, define an enum of failure kinds and implement std::fmt::Display and std::error::Error for it, plus From conversions so ? works across sources:

#[derive(Debug)]
enum ConfigError { Io(std::io::Error), Parse(std::num::ParseIntError) }
impl From<std::io::Error> for ConfigError {
    fn from(e: std::io::Error) -> Self { ConfigError::Io(e) }
}

The thiserror crate generates that boilerplate for libraries. In applications, anyhow::Result holds any error with added context (.context("loading config")), and is the usual choice. Rule of thumb: typed errors where callers must react to the kind, anyhow where they only report it.

Practice. Create a ConfigError enum with Io and Parse variants, then rewrite it using thiserror.

Stage 6 Traits, Generics and Smart Pointers

Intermediate2-3 weeks6 topics

Goal. Write generic code with trait bounds, use trait objects, and choose between Box, Rc and Arc.

Trait Definitions and Implementations Core

A trait is a set of methods a type can promise to provide, much like an interface. Define with trait Summary { fn summarise(&self) -> String; }, implement with impl Summary for Post { ... }. Traits may have default methods. The orphan rule says you can implement a trait for a type only if you own the trait or the type, which prevents conflicts between crates.

Traits you meet constantly: Debug, Display, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, From, Into, Iterator, Drop. Most can be derived. Implementing Iterator by writing only next gives you dozens of adapter methods for free. Operators such as + are traits in std::ops.

Practice. Define a Shape trait with area, implement it for two structs and write a function that takes any Shape.

Traits and Generics: Trait Bounds and Associated Types Core

Generics let one function or type work for many types. Bounds say what the type must do: fn largest<T: PartialOrd>(xs: &[T]) -> &T, or with a where clause for readability. impl Trait in argument position is shorthand for a bound, and in return position hides a concrete type, as in returning a closure or iterator.

Rust generics are monomorphised: the compiler generates a specialised copy for each concrete type, so there is no run-time cost, but binaries can grow. Associated types, such as type Item in Iterator, let a trait name a related type once instead of making it another parameter. Use generic parameters when callers choose the type and associated types when the implementor does.

Practice. Write a generic function that returns the largest element of a slice and one that sums any iterator of numbers.

Trait Objects and Dynamic Dispatch Core

Generics resolve at compile time. When you need values of different concrete types in one collection, use a trait object: Vec<Box<dyn Shape>>. Calls go through a vtable (a table of function pointers) at run time, which costs a small indirect call and prevents inlining, but keeps code size down and allows heterogeneous collections.

A trait is object safe only if its methods do not return Self or have generic type parameters. Choose static dispatch (generics) for speed and dynamic dispatch (dyn) for flexibility such as plugin systems. In interviews, explain this trade-off by comparing it to virtual functions in Modern C++ Roadmap.

Practice. Store a mix of circles and squares in one vector of boxed trait objects and print the total area.

Advanced Generics and Type-level Programming Optional

Beyond simple bounds, Rust offers const generics (struct Matrix<const R: usize, const C: usize>), generic associated types, higher-ranked trait bounds (for<'a> Fn(&'a str)), marker traits and zero-sized types. Together they let you encode rules in types.

A popular technique is the typestate pattern: a builder or connection whose type changes as it moves through states, so calling methods in the wrong order does not compile. PhantomData<T> marks a type parameter that is not stored in a field. Use these only where the guarantee is worth the added complexity; unreadable type signatures are a cost of their own.

Practice. Build a typestate builder where build() exists only after the required fields are set.

Covariant and Contravariant Lifetimes Optional

Variance describes how subtyping of lifetimes carries over to a generic type. A longer-lived reference can stand in for a shorter-lived one, so &'long T can be used where &'short T is expected. This is covariance, and most types behave this way.

Mutable references &mut T are invariant in T: if they were not, you could write a short-lived value into a place expecting a long-lived one. Function arguments are contravariant, and Cell<T> and *mut T are invariant. You will rarely write this by hand, but it appears when you build unsafe abstractions and in compiler errors about "lifetime may not live long enough". Know the idea well enough to explain why &mut is stricter.

Practice. Explain in your own words why a mutable reference to a vector of short-lived string slices cannot be treated as one holding longer-lived slices.

Smart Pointers: Box, Rc, Arc and RefCell Core

Box<T> owns a value on the heap; use it for recursive types (a list node containing a Box<Node>), large values and trait objects. Rc<T> is a reference-counted pointer allowing several owners in one thread; Rc::clone copies the pointer and bumps the count, and the value is dropped at zero. Arc<T> is the thread-safe version using atomic counts.

Both give shared read-only access. For mutation through a shared pointer, use interior mutability: RefCell<T> checks borrow rules at run time and panics on violation (single thread); Mutex<T> does it across threads. Rc<RefCell<T>> is the classic combination, and cycles of Rc leak memory; break them with Weak<T>. Reach for these only after simpler ownership has failed.

Practice. Build a singly linked list with Box, then a tree where children hold Rc and a Weak link to the parent.

Stage 7 Concurrency and Async

Advanced3 weeks5 topics

Goal. Write thread-safe programs and async services, and explain why Rust prevents data races.

Concurrency and Parallelism: Threads, Channels, and Message Passing Core

std::thread::spawn starts an OS thread running a closure, and join waits for it. Closures that cross threads usually need move to take ownership of what they use. Scoped threads (std::thread::scope) can borrow local data safely because they are joined before the scope ends.

Message passing uses channels: std::sync::mpsc::channel() gives a sender and receiver; the sender can be cloned for many producers. The motto is to share memory by communicating. Rust prevents data races at compile time using two marker traits: Send (safe to move to another thread) and Sync (safe to share by reference). Rc is neither, so using it across threads is a compile error. The rayon crate turns iterator chains into parallel ones with par_iter.

Practice. Spawn four threads that each sum part of a large vector and send the partial sums over a channel.

Shared State: Arc, Mutex and RwLock Core

To share mutable data across threads, wrap it as Arc<Mutex<T>>. lock() returns a guard that gives access and releases the lock when dropped, so you cannot forget to release it:

let counter = Arc::new(Mutex::new(0));
let c = Arc::clone(&counter);
thread::spawn(move || { *c.lock().unwrap() += 1; });

If a thread panics while holding the lock, the mutex becomes poisoned and lock returns an error. RwLock allows many readers or one writer. Keep critical sections short, never hold a guard across an .await, and avoid taking two locks in different orders, which deadlocks. For plain counters and flags, atomics are lighter.

Practice. Increment a shared counter from eight threads with Arc and Mutex, then replace it with an atomic.

Atomic Operations and Memory Barriers Optional

Types in std::sync::atomic (AtomicUsize, AtomicBool, AtomicPtr) support lock-free operations like fetch_add and compare_exchange. Each takes an Ordering that says how the operation is ordered relative to other memory accesses. Relaxed guarantees only atomicity; Acquire and Release pair up so that writes before a release are visible after the matching acquire; SeqCst gives a single global order and is the safest default.

Memory barriers (fences) enforce ordering between operations without touching a specific variable. Rust uses the same memory model as C++20, so the explanation in the Modern C++ Roadmap roadmap applies. Use SeqCst unless you can justify a weaker ordering and have tested with tools such as Loom.

Practice. Implement a spin flag with AtomicBool using Release on store and Acquire on load, and explain why Relaxed would not be enough.

Asynchronous Programming: Futures and Async/Await Paradigm Core

Async code lets one thread run many tasks that spend most of their time waiting on I/O. An async fn returns a Future, a value that does nothing until it is polled; .await suspends the current task until the future is ready, letting the executor run others.

The language provides the syntax and the Future trait, but no executor, so you add a runtime crate. Futures are lazy: calling an async function without awaiting it does nothing. They compile into state machines, which makes them cheap but means self-referential data needs pinning (Pin). Never block the thread inside async code with long computation or std::thread::sleep; use async equivalents or move the work to a blocking thread pool.

Practice. Write an async function that fetches two values concurrently with join and prints the total time.

Async Runtimes: Tokio, async-std and smol Core

Tokio is the dominant runtime: multi-threaded work-stealing scheduler, timers, sockets, channels, select! and spawn. Mark main with #[tokio::main]. Most of the ecosystem (hyper, axum, reqwest, tonic) builds on it.

#[tokio::main]
async fn main() {
    let h = tokio::spawn(async { 21 * 2 });
    println!("{}", h.await.unwrap());
}

async-std mirrors the standard library API in async form and has seen little development lately. smol is a small, simple runtime with a similar goal. For new projects, choose Tokio unless you have a reason not to. Learn tokio::select! for racing futures, tokio::time::timeout, bounded mpsc channels for back-pressure and spawn_blocking for synchronous work.

Practice. Build a small TCP echo server with Tokio that handles many connections and a 10 second idle timeout.

Stage 8 Macros, Testing and Tooling

Advanced2-3 weeks10 topics

Goal. Test, document, profile and publish Rust code, and read and write simple macros.

Declarative Macros with macro_rules! Recommended

Macros generate code at compile time. A declarative macro matches token patterns and expands to code:

macro_rules! square {
    ($x:expr) => { $x * $x };
}

Fragment types such as expr, ident, ty and tt say what each capture matches, and repetition like $( $x:expr ),* handles lists, which is how vec! works. Macros are hygienic: local names inside a macro do not clash with the caller's.

Use a macro only when a function or generic cannot do the job, for example for variable argument counts or repetitive impl blocks. Errors inside macros are harder to read, so keep them short.

Practice. Write a macro that builds a HashMap from key => value pairs, like a map literal.

Procedural Macros and Custom Derive Optional

Procedural macros are Rust functions that receive a token stream and return a new one. There are three kinds: derive macros (#[derive(Serialize)]), attribute macros (#[tokio::main], #[route(GET, "/")]) and function-like macros (sql!(...)). They live in a separate crate marked proc-macro = true, and are usually written with the syn crate to parse input and quote to generate output.

They power Serde, clap and many frameworks. Costs: slower compile times and tricky debugging; cargo expand shows the generated code. Knowing what they do is enough for most roles; writing one is a stretch topic.

Practice. Use cargo expand on a struct with a derive and read what the macro generated.

Macros and Metaprogramming, Domain-Specific Languages (DSLs) Optional

Macros and builders make small domain-specific languages (DSLs) in Rust: HTML templates in Leptos, query builders in Diesel, argument definitions in clap and test tables. A good DSL reads like the domain while still being checked by the compiler.

Prefer builders and plain functions first; they are easier to debug and document. Reach for macros when the syntax you want cannot be expressed as normal code, and keep the macro layer thin, forwarding to ordinary functions. Provide examples in docs, and make error messages point to the user's code rather than internals.

Practice. Design a small builder-style DSL for describing an HTTP request and compare it with a macro version.

Testing: Unit and Integration Testing Core

Unit tests sit in the same file inside #[cfg(test)] mod tests, marked with #[test], and can test private functions. Integration tests live in the tests/ folder and use only your public API. Doc tests, written as code blocks in documentation, are compiled and run, which keeps examples correct. Run all with cargo test; filter by name; -- --nocapture shows output.

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn adds() { assert_eq!(add(2, 2), 4); }
    #[test]
    #[should_panic]
    fn fails() { divide(1, 0); }
}

Tests can return Result<(), E> to use ?. Use assert!, assert_eq! and assert_ne! with custom messages.

Practice. Write unit tests including a failing-input case, an integration test and a doc test for one function.

Mocking and Property-based Testing Recommended

Mocking in Rust normally means putting a trait in front of the dependency and writing a fake that implements it. The mockall crate generates mocks for traits when hand-written fakes become tedious. Avoid mocking everything; test pure logic directly.

Property-based testing generates many random inputs and checks that a property always holds, then shrinks failures to a minimal example. The proptest and quickcheck crates provide it. Properties such as "decoding an encoded value returns the original" or "sorting is idempotent" find edge cases that hand-picked examples miss. Fuzzing with cargo fuzz goes further for parsers and untrusted input.

Practice. Write a proptest property that checks parse(format(n)) == n for a number type.

Documenting with rustdoc Recommended

Doc comments start with /// for the next item or //! for the enclosing module or crate, and use Markdown. cargo doc --open builds HTML documentation, including dependencies. Code blocks in doc comments are compiled and executed by cargo test, which keeps examples honest. Common sections are # Examples, # Errors, # Panics and # Safety for unsafe functions.

Link to other items with square brackets such as [HashMap]. Add #![warn(missing_docs)] to a library so undocumented public items produce warnings. Published crates get their documentation hosted automatically by the registry's docs service.

Practice. Document a small public function with an example, an Errors section and run cargo test to verify the example.

Debugging with rust-gdb and rust-lldb Recommended

rust-gdb and rust-lldb are wrappers that load pretty-printers so Vec, String and HashMap appear readable in GDB or LLDB. Build with debug info (the default cargo build), run rust-gdb target/debug/app, then use break, run, next, step, print and bt. VS Code with the CodeLLDB extension gives a graphical experience.

Set RUST_BACKTRACE=1 to print a stack trace on panic, and RUST_LOG with the env_logger or tracing crates for logs. dbg!(expr) prints the expression, its value and the location, then returns the value, which is quicker than println-debugging. Compiler errors often include a help section that suggests the fix; read it first.

Practice. Debug a program that panics using RUST_BACKTRACE, then step through it in a debugger.

Performance and Profiling with Criterion.rs Recommended

Always measure release builds. In Cargo.toml, [profile.release] options like lto = "fat", codegen-units = 1 and panic = "abort" trade compile time for speed or size. Use cargo flamegraph or perf to see where time goes, and cargo bench or Criterion.rs for statistical micro-benchmarks that compare runs and detect regressions.

Frequent wins: avoid needless clones and allocations, reserve capacity, prefer iterators, use &str over String in APIs, pick a faster hasher, and stay cache-friendly with contiguous data. Do not guess; profile first. For binary size, strip = true and opt-level = "z" help in embedded and WebAssembly builds.

Practice. Benchmark two string-building approaches with Criterion and read its report.

Publishing on crates.io Optional

Crates are shared through the central registry, crates.io. Before publishing, fill in Cargo.toml fields: name, version, description, licence, repository, and optionally readme and keywords. Run cargo publish --dry-run, then cargo publish with an API token. Published versions cannot be deleted, only yanked, which stops new projects from depending on them.

Follow semantic versioning: breaking API changes need a major bump (a minor bump below 1.0). cargo semver-checks can detect accidental breaks. Keep a changelog, document features and set a minimum supported Rust version. For private code use a private registry or git dependencies.

Practice. Prepare a small crate for publishing and run a dry run, fixing every warning.

Unsafe Rust and FFI Optional

The unsafe keyword allows five extra operations: dereferencing raw pointers, calling unsafe functions, accessing mutable statics, implementing unsafe traits and reading union fields. It does not turn off the borrow checker; it says "the programmer guarantees these rules".

The usual pattern is a small unsafe core wrapped in a safe API, with a comment explaining why it is sound. FFI (foreign function interface) calls C through extern "C" blocks and #[repr(C)] structs; bindgen generates bindings and cbindgen exports headers for C callers. Test unsafe code under Miri, an interpreter that detects undefined behaviour. In most application code you will never need it.

Practice. Wrap a call to a C library function such as strlen in a safe Rust function and test it under Miri if you can.

Stage 9 Ecosystem and Applications

Advanced4 weeks10 topics

Goal. Choose and use the main crates for data formats, command line tools, web services, databases, WebAssembly, embedded work, games and security.

Serialization/Deserialization with Serde, JSON and TOML Parsing Core

Serde is the standard framework for serialisation and deserialisation. Derive Serialize and Deserialize on your types, then pick a format crate: serde_json for JSON, toml for TOML parsing (the format of Cargo.toml and many config files), serde_yaml, bincode, csv and others. Attributes like #[serde(rename = "id")], default, skip_serializing_if and flatten control the mapping.

#[derive(Serialize, Deserialize)]
struct Config { port: u16, #[serde(default)] debug: bool }
let c: Config = toml::from_str(text)?;

Json-rust is an older standalone JSON crate, but Serde is the usual choice now. Validate input at the boundary, and deserialise untrusted data into strict types.

Practice. Load a TOML config into a struct with defaults, and write the same struct out as JSON.

CLI Utilities: clap, StructOpt and Termion Recommended

clap is the standard argument parser. With its derive API, a struct describes the options and clap generates parsing, help text, validation, sub-commands and shell completion:

#[derive(Parser)]
struct Args { #[arg(short, long)] name: String, count: Option<u32> }
let args = Args::parse();

StructOpt was an earlier derive-based crate; its features were merged into clap version 3 and later, so new code should use clap directly. For terminal control (raw mode, colours, cursor), Termion works on Unix, while crossterm also supports Windows; ratatui builds full-screen interfaces. Good CLIs print errors to standard error and use proper exit codes.

Practice. Build a tool with two sub-commands using clap derive, and colour its output.

Web Development: Axum, Actix, Rocket and hyper Core

Hyper is the low-level HTTP library most frameworks use. Axum, built by the Tokio team on hyper and tower, is a popular choice with extractor-based handlers and middleware from the tower ecosystem. Actix Web is a mature, very fast framework with its own actor-based heritage. Rocket emphasises ease of use with macros for routing.

let app = Router::new().route("/hello/{name}", get(hello));
async fn hello(Path(name): Path<String>) -> String { format!("Hi {name}") }

Loco is a Rails-like full-stack framework on top of Axum. For browser clients, reqwest is the common HTTP client. Full-stack and frontend options such as Leptos compile Rust to WebAssembly for interactive pages. See API Design Roadmap for design questions beyond the code.

Practice. Build a JSON API with two routes in Axum, shared state behind an Arc and a request timeout.

Database and ORM: sqlx, Diesel and rusqlite Recommended

sqlx is an async SQL toolkit that checks queries against a real database at compile time with its query! macro, without an ORM layer. Diesel is a synchronous ORM and query builder that uses the type system to catch invalid queries. SeaORM offers an async ORM option. rusqlite wraps SQLite for embedded use and tokio-postgres talks to PostgreSQL directly.

Choose by taste: raw SQL with sqlx keeps you close to the database, Diesel gives strong type safety at the cost of a steeper learning curve. Use connection pools, migrations kept in version control and transactions for multi-step changes. See SQL Roadmap for the language itself.

Practice. Create a table, insert rows and query them with sqlx against SQLite, using a migration file.

Networking and QUIC Optional

The standard library has blocking TCP and UDP sockets (TcpListener, TcpStream, UdpSocket). Tokio supplies async versions and frameworks around them. Higher-level crates cover gRPC (tonic), WebSockets (tokio-tungstenite) and TLS (rustls, a pure Rust implementation).

quinn implements QUIC, the UDP-based transport used by HTTP/3, with multiplexed streams that avoid head-of-line blocking and built-in encryption. Learn framing (length prefixes), timeouts, back-pressure and graceful shutdown; those matter more than the library.

Practice. Write a length-prefixed message protocol over TCP and a client and server that exchange a few messages.

WebAssembly: wasm-bindgen, wasm-pack and Wasmer Optional

Rust compiles to WebAssembly (WASM), a compact binary format that runs in browsers and other hosts. Add the target with rustup target add wasm32-unknown-unknown. wasm-bindgen generates the glue so Rust and JavaScript can call each other with strings and objects, and wasm-pack builds, packages and publishes the result for npm.

Outside the browser, WASM runs in runtimes such as Wasmer and Wasmtime, which makes it a sandboxed plugin format; the WASI target covers system access. Typical uses are heavy computation in web apps, image or crypto routines and plugin systems. See JavaScript Roadmap for the JavaScript side.

Practice. Compile a function that counts primes to WASM and call it from a web page.

Embedded and Systems Programming Optional

Rust fits microcontrollers because it needs no runtime. Programs use #![no_std], which drops the standard library and keeps core; an allocator is optional. The embedded-hal crate defines traits for GPIO, SPI, I2C and timers, so drivers work across chips. Vendor-specific hardware abstraction crates such as nrf-hal implement them for particular families, and rppal gives access to Raspberry Pi pins from Linux.

Tooling includes probe-rs for flashing and debugging, and Embassy, an async framework for microcontrollers. Rust's guarantees help against memory bugs, but you still reason about interrupts and shared hardware state. See Embedded Systems Engineer for the career path.

Practice. Blink an LED using embedded-hal on a development board, or simulate it in an emulator.

Game Development: bevy, Fyrox, ggez and macroquad Optional

Rust's game ecosystem is young but active. Bevy is a data-driven engine built on an entity component system (ECS), where data lives in components and logic in systems. Fyrox is a more traditional engine with a scene editor. ggez is a lightweight 2D framework in the style of LÖVE, and macroquad is a simple, fast-to-start library with minimal dependencies. For graphics, wgpu-rs is the cross-platform GPU API underneath many of them.

ECS suits Rust's ownership rules, because systems declare what data they read or write and the scheduler can run them in parallel. Start with macroquad or ggez for a first game and move to Bevy for larger ones. See Game Developer for roles.

Practice. Make a simple Pong or Snake game in macroquad, then think about how you would model it as ECS.

GUI Development: Tauri, Iced, GPUI and gtk-rs Optional

Desktop options differ in approach. Tauri builds desktop apps with a web frontend and a Rust backend, producing small installers because it uses the system web view. Iced follows the Elm architecture in native Rust. GPUI is a GPU-accelerated UI framework from the Zed editor team. gtk-rs binds GTK, egui offers immediate-mode interfaces and Slint and Qt bindings also exist.

No option is as mature as the main web or native toolkits. Pick Tauri if you know web UI, and a native crate if you want one language end to end. Keep logic in a separate crate so the UI layer stays swappable.

Practice. Build a counter window in Iced, or a small Tauri app that calls a Rust command.

Cryptography: ring, sodiumoxide and Friends Optional

Never write your own cryptography. Use audited crates: ring provides hashing, AEAD encryption, signatures and key agreement with a small, opinionated API. RustCrypto publishes separate crates for many algorithms. The old rust-crypto crate is unmaintained and has known issues, so avoid it. sodiumoxide wraps libsodium but is deprecated in favour of alternatives such as dryoc or the RustCrypto family; check the status before choosing.

Use argon2 for password hashing, constant-time comparisons for secrets, zeroize to clear key material and the operating system's random source through rand/getrandom. Do not invent protocols; use TLS (rustls) for transport. See Security Engineer.

Practice. Hash a password with argon2 and verify it, and encrypt a short message with an AEAD from a maintained crate.

Projects to build

Build these in order. Each one should end up in a public repository with a short README that explains your decisions.

  1. Command line grep cloneBeginner

    Write a tool that searches files for a pattern and prints matching lines with line numbers, with flags for case-insensitive search and recursion. Use clap for arguments and give clear errors for unreadable files.

    Cargo project · Strings and slices · Result and the ? operator · clap

  2. Library management data modelBeginner

    Model books, members and loans with structs and enums, store them in collections, and implement borrow and return rules that reject invalid actions. Cover the rules with unit tests.

    Structs and enums · HashMap and Vec · Pattern matching · Unit tests

  3. Tokenizer and expression calculatorIntermediate

    Parse arithmetic expressions into tokens, build a syntax tree stored in a Box-based enum and evaluate it. Report errors with positions using a custom error type.

    Enums and Box · Traits · Custom errors · Recursion

  4. Concurrent web crawlerIntermediate

    Fetch pages starting from one address using Tokio and reqwest with a limit on parallel requests, a visited set shared safely, and a timeout on each request. Print a summary of discovered links.

    Async and Tokio · Arc and Mutex · Channels · reqwest

  5. JSON API with a databaseAdvanced

    Build an Axum service for notes with create, read, update and delete, backed by SQLite or PostgreSQL through sqlx. Add input validation, structured logging, integration tests and a Docker image.

    Axum · sqlx · Serde · Integration tests

  6. Key-value storeAdvanced

    Write a small persistent store with an append-only log, an in-memory index, compaction and a TCP protocol. Benchmark it with Criterion and check for races with careful design.

    File I/O · Concurrency · Performance · Error handling

Interview questions

Try to answer each question out loud before you open the answer.

What are the rules of ownership in Rust?Junior

Every value has a single owner, and when that owner goes out of scope the value is dropped. Assigning or passing a non-Copy value moves ownership, so the old binding becomes unusable. Because the compiler knows where each value is freed, there is no garbage collector and no manual free, and double frees and use-after-free are rejected at compile time.

What is the difference between move, clone and copy?Junior

A move transfers ownership and leaves the source unusable; it copies only the shallow bits. Clone is an explicit, possibly expensive duplicate of the data, such as copying a string's heap buffer. Copy is a marker trait for simple types that are duplicated implicitly on assignment, like integers and bool. A type that owns heap data or implements Drop cannot be Copy.

Explain shared and mutable references.Junior

A shared reference &T allows reading and may exist many times; a mutable reference &mut T allows writing and must be the only reference to that value while it is live. This aliasing XOR mutation rule is checked at compile time and prevents data races and problems like pushing into a vector while iterating over it. Borrows end at their last use, not at the end of the scope.

What is the difference between String and &str?Junior

String is an owned, growable, heap-allocated UTF-8 buffer. &str is a borrowed slice of UTF-8 text: a pointer and a length, which may point into a String, a literal or other memory. Functions that only read text should take &str so callers can pass either. You cannot index them by integer because characters have variable byte widths.

How does Rust handle null and errors?Junior

There is no null. Absence is Option<T> and failure is Result<T, E>, both enums the compiler makes you handle. The ? operator returns early on error and converts error types with From. Panics exist for bugs and broken invariants, and unwrap or expect should not be used for expected failures in production code.

What is a lifetime, and why does the compiler sometimes ask for annotations?Mid

A lifetime is the region of code where a reference is valid. The compiler checks that references never outlive what they point to. Usually it infers them using elision rules. When a function returns a reference and there are several possible sources, it cannot tell which input it borrows from, so you annotate the relation, as in fn pick<'a>(a: &'a str, b: &'a str) -> &'a str. Annotations describe relationships; they do not extend lifetimes.

Compare generics and trait objects.Mid

Generics use static dispatch: the compiler creates a specialised copy per type, which allows inlining and has no run-time overhead but increases code size. Trait objects (dyn Trait) use a vtable for dynamic dispatch, allowing different types in one collection and smaller code at the cost of an indirect call and some restrictions on object safety. I default to generics and use dyn for heterogeneous collections and plugin boundaries.

When would you use Rc, Arc, RefCell and Mutex?Mid

Rc gives shared ownership within one thread and Arc does the same across threads using atomic counts. Neither allows mutation by itself. RefCell adds mutation through shared references with borrow checks at run time, for single-threaded code; Mutex and RwLock do it across threads. Typical combinations are Rc<RefCell<T>> and Arc<Mutex<T>>. I first try to restructure ownership so none are needed.

What do Send and Sync mean?Mid

Send means a value can be moved to another thread. Sync means a shared reference to it can be used from several threads, that is, &T is Send. The compiler implements them automatically for types made of Send or Sync parts. Rc is neither, so it cannot cross threads, and Arc<Mutex<T>> is both when T is Send. These marker traits are how the compiler rejects data races.

How does async/await work in Rust?Mid

An async function returns a future, a state machine that does nothing until polled. .await yields control when the result is not ready so the executor can run other tasks, and wakes the task when the I/O completes. The language has no built-in executor; a runtime like Tokio drives futures. Blocking calls inside async code stall the worker thread, so I use async APIs or spawn_blocking.

How would you design error handling for a library versus an application?Mid

For a library I define a typed error enum implementing Error and Display, with From conversions for sources, so callers can match on kinds; thiserror removes the boilerplate. For an application I use anyhow to carry any error with added context and report it at the top. I avoid panicking in libraries and keep error messages specific about what operation failed.

What is unsafe Rust and when is it justified?Senior

unsafe allows dereferencing raw pointers, calling unsafe functions, accessing mutable statics, implementing unsafe traits and reading union fields. It does not disable the borrow checker; it moves the proof burden to me. It is justified for FFI, low-level data structures and performance primitives, wrapped in a small module with a safe API, documented invariants and tests under Miri.

Why can a program with a Vec of references fail to compile when you push after taking a reference?Mid

Taking &v[0] borrows the vector. Pushing may reallocate the buffer, which would leave the reference dangling. The compiler therefore rejects push while the earlier shared borrow is still used later. The fix is to finish using the reference first, copy or clone the element, or use indexes instead of references.

How do you diagnose and improve performance in a Rust program?Senior

I measure release builds first with Criterion for micro-benchmarks and a profiler or flamegraph for hotspots. Common fixes are removing unnecessary clones and allocations, preallocating with with_capacity, borrowing instead of owning, using iterators, choosing better data layouts and a faster hasher, and parallelism with rayon. I enable LTO and a single codegen unit for final builds, and re-measure after each change.

Explain the typestate pattern and why you might use it.Senior

You encode a state in the type, so that methods exist only in valid states. A connection builder might return Connection<Closed> from new, and only Connection<Open> has send. Transitions consume self and return the next type. Invalid sequences become compile errors, not run-time checks. The cost is more types and generics, so I use it where misuse is costly.

Common mistakes

  • Fighting the borrow checker with clones everywhere; first reshape ownership, and clone only when you understand why.
  • Using unwrap and expect for expected failures; return Result and use the ? operator instead.
  • Taking String or Vec parameters when a &str or &[T] would do; borrow in function signatures.
  • Holding a Mutex guard across an await point or for a long time; scope the lock tightly.
  • Blocking inside async functions with sleeps or heavy computation; use async APIs or spawn_blocking.
  • Reaching for Rc<RefCell<T>> or unsafe early; redesign the data flow before using them.
  • Benchmarking debug builds; always use release mode and a proper benchmarking tool.
  • Adding explicit lifetimes everywhere; rely on elision and annotate only where the compiler asks.
  • Writing clever macros or generic code before plain code works; start simple and generalise later.

Ready-for-interview checklist

  • I can explain ownership, moves, borrowing and why each rule exists.
  • I can fix common borrow checker errors without cloning blindly.
  • I can model a problem with structs and enums and use exhaustive match.
  • I can use Option and Result combinators and the ? operator.
  • I can define and implement traits, and choose between generics and trait objects.
  • I can explain when to use Box, Rc, Arc, RefCell and Mutex.
  • I can write a custom error type and convert errors with From.
  • I can spawn threads, use channels and share state safely.
  • I can write async code with Tokio and explain what a future is.
  • I can write unit, integration and doc tests, and a property test.
  • I can read a flamegraph and use Criterion to measure a change.
  • I can build a small CLI and a web API with the common crates.
  • I can explain what unsafe allows and how to contain it.

Related roadmaps

FAQ

How long does it take to learn Rust?

Expect two to three weeks to be writing small programs and another few weeks before ownership and lifetimes feel natural. Reaching interview level, including traits, error handling and concurrency, takes about fourteen to twenty weeks at eight hours a week if you build projects.

Is Rust hard to learn?

It is harder at the start than most languages because the compiler enforces rules about memory that other languages leave to you. Most of the difficulty is concentrated in ownership and lifetimes. Once those are clear, everyday code feels straightforward, and many people find they write fewer bugs.

Should I learn C or C++ before Rust?

You do not need to. Knowing how memory, pointers and the stack work helps, and the Modern C++ Roadmap roadmap covers a neighbouring language, but Rust can be learned first if you are comfortable with another language.

Is Rust worth learning for a job?

It is worth it for systems, infrastructure, security-sensitive, embedded and performance-critical roles, and its use is growing in those areas. There are fewer openings than for older languages, so treat it as a strong addition to a main skill, not a replacement.

Rust or Go: which should I learn first?

Choose Go for quick progress on network services and a gentle learning curve. Choose Rust for fine control over memory, no garbage collector pauses and stronger compile-time guarantees. The Go Roadmap roadmap covers the first path.

Practise while you learn.Solve interview problems in the browser, with hints and a timer.
CodingSQLSystem design
Next Post Previous Post
No Comment
Add Comment
comment url