Step 01 of 17
Getting started
Install Rust with rustup, create and run a project with Cargo, and learn to read the compiler's error messages.
Rust gives you the speed and control of C and C++ with a guarantee neither offers: if your program compiles (and you haven't written unsafe), it has no use-after-free, no double free and no data races. There's no garbage collector to pause it. The price is a stricter compiler, and the way to pay it cheaply is to treat that compiler as a collaborator. This lesson installs the toolchain, walks through a Cargo project, explains what happens when you build, and teaches you to read the errors you'll spend your first weeks with.
Why Rust, and when not
Most mainstream languages make you choose. Go, Java, C# and Python manage memory for you with a garbage collector (or reference counting), which is safe but costs memory and adds pauses. C and C++ hand you raw control and fast binaries, and with them a long history of security bugs: roughly 70% of the serious vulnerabilities Microsoft and Google report in their C/C++ code are memory-safety bugs.
Rust takes a third route. Every value has exactly one owner, and the compiler tracks when that owner goes away. It inserts the code that frees memory at that point, at compile time. It also checks that no reference outlives the data it points to, and that nothing is mutated while something else is reading it. You'll learn these rules in the ownership and borrowing lessons.
What you get:
- Performance on par with C++: no runtime, no GC, zero-cost abstractions (iterators and generics compile down to the loops you'd write by hand).
- Reliability: no null (you use
Option), no exceptions (you useResult), and exhaustive pattern matching. - One toolchain: build, test, format, lint, docs and dependency management all ship with it.
What it costs: a steeper first month, and slower compile times than Go. If you're writing a small CRUD service and your team knows Go or TypeScript, Rust may not pay for itself. It shines in CLIs, networked services with tight latency or memory budgets, embedded systems, WebAssembly, databases, and anything you'd otherwise write in C++.
Installing the toolchain
Install Rust with rustup, the official installer and version manager. Don't use your OS package manager: its Rust is usually old, and rustup manages several toolchains side by side.
# macOS / Linux
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Windows: download and run rustup-init.exe from rustup.rsThat installs a set of tools into ~/.cargo/bin:
| Tool | What it does |
|---|---|
rustc | The compiler. You'll rarely call it directly. |
cargo | Build tool and package manager. You'll call it constantly. |
rustfmt / cargo fmt | The standard code formatter. |
clippy / cargo clippy | A linter with hundreds of checks. |
rustup | Installs and updates toolchains and components. |
rustc --version # rustc 1.9x.0 (...)
rustup update # move to the latest stable (a new one ships every 6 weeks)
rustup component add rust-analyzer # language server for your editorInstall the rust-analyzer extension in your editor (VS Code, Zed, Neovim, JetBrains RustRover has its own). It shows inferred types inline and runs cargo check on save. With Rust, seeing types you didn't write is worth a lot.
Your first Cargo project
cargo new hello
cd hellohello/
├── Cargo.toml
├── .gitignore
└── src/
└── main.rsCargo.toml is the manifest: the package's name, version, edition and dependencies.
[package]
name = "hello"
version = "0.1.0"
edition = "2024"
[dependencies]src/main.rs holds the entry point:
fn main() {
println!("Hello, world!");
}fn declares a function. main is where a binary starts. println! ends with ! because it's a macro, not a function: it's expanded at compile time, which is how it can take a variable number of arguments and check the format string against them.
Run it:
$ cargo run
Compiling hello v0.1.0 (/home/you/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/hello`
Hello, world!The commands you'll use every day:
| Command | What it does |
|---|---|
cargo check | Type-checks without producing a binary. The fastest feedback loop. |
cargo build | Debug build into target/debug/. |
cargo run | Build, then run. Arguments after -- go to your program. |
cargo build --release | Optimized build into target/release/. Often 10–50× faster at runtime. |
cargo test | Build and run every test. |
cargo fmt / cargo clippy | Format and lint. |
cargo add serde | Add a dependency to Cargo.toml. |
Formatting values
println! and its siblings (print!, eprintln!, format!) use {} placeholders:
fn main() {
let name = "Ada";
let year = 1843;
let langs = vec!["Rust", "Go"];
println!("{name} wrote the first program in {year}"); // inline variables
println!("{} + {} = {}", 2, 2, 2 + 2); // positional
println!("{langs:?}"); // Debug format: ["Rust", "Go"]
println!("{:>8}|{:<8}|{:^8}|", "right", "left", "mid"); // alignment
println!("{:.3}", std::f64::consts::PI); // 3.142
let s = format!("{name}-{year}"); // build a String instead of printing
eprintln!("this goes to stderr: {s}");
}{} uses the Display trait, which is for user-facing output; numbers and strings have it. {:?} uses Debug, which is for developers; almost every standard type has it, and you can add it to your own types with #[derive(Debug)]. Use {:#?} for a pretty-printed, multi-line version.
Reading compiler errors
The compiler will reject a lot of your code at first. Rust's error messages are unusually good, and reading them properly is the most useful skill in this lesson. Take this program:
fn main() {
let count = 0;
count += 1;
println!("{count}");
}error[E0384]: cannot assign twice to immutable variable `count`
--> src/main.rs:3:5
|
2 | let count = 0;
| ----- first assignment to `count`
3 | count += 1;
| ^^^^^^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut count = 0;
| +++Read it in this order:
- The headline and code.
E0384tells you the kind of error.rustc --explain E0384prints a full explanation with examples. - The location.
src/main.rs:3:5is file, line and column. - The labels.
^^^marks the primary problem;---marks related places (here, where the variable was declared). - The
help:. Often a literal diff to apply. Here: addmut.
Two habits make this much easier. First, fix the first error first: later errors are often caused by earlier ones. Second, run cargo check constantly instead of waiting to build the whole thing. When an error mentions a type you don't recognize, hover over the variable in your editor to see what rust-analyzer inferred.
Practice
- Install Rust with rustup and add rust-analyzer to your editor.
- Create a project with
cargo new greet. - Make
mainread the first command-line argument and printHello, <name>!, orHello, world!if there's none. Usestd::env::args(), which returns an iterator ofStrings whose first item is the program's own path. Look upIterator::nthwith your editor's hover docs. - Run it with
cargo run -- Grace. - Break it on purpose: try to assign to an immutable variable, and call a function that doesn't exist. Read both errors in full, and run
rustc --explainon one of the codes. - Run
cargo fmtandcargo clippy, then build with--releaseand compare the file sizes intarget/debugandtarget/release.
Show solution
use std::env;
fn main() {
// nth(1) skips the program path at index 0.
let name = env::args().nth(1).unwrap_or_else(|| String::from("world"));
println!("Hello, {name}!");
}$ cargo run -q -- Grace
Hello, Grace!
$ cargo run -q
Hello, world!nth(1) returns an Option<String>: Some(name) if there was an argument, None otherwise. unwrap_or_else takes the value out of Some, or calls the closure to build a default for None. You'll meet Option properly in the structs and enums lesson.
The release binary is usually smaller than the debug one because it carries no debug info, and much faster for anything that does real work.
Key takeaways
- Rust gets memory safety from compile-time ownership rules instead of a garbage collector, so it's fast, predictable and safe.
- Install with rustup; use Cargo for everything:
check,run,test,build --release,fmt,clippy,add. - Each crate picks an edition in
Cargo.toml; this roadmap uses 2024 (Rust 1.85+). println!is a macro;{}usesDisplay,{:?}usesDebug.- Read errors top to bottom — headline, location, labels,
help:— and fix the first one first.
Check yourself
Quiz
0/4 correct · 4 left
1.Which command gives you the fastest feedback that your code compiles?
2.Why does println! end with an exclamation mark?
3.A colleague says Rust is slow and shows a benchmark of target/debug/app. What's the most likely problem?
4.What does the 2024 edition in Cargo.toml mean?
Finished reading?
Mark it complete to fill in your roadmap.